18 Commits

Author SHA1 Message Date
Cai
643c16fdf7 fix:课时编辑页面步骤处保存冗余保存问题 2026-01-05 17:26:14 +08:00
Cai
5082436289 fix:index.html的课程详情页面跳转缺失 2026-01-05 16:08:55 +08:00
Cai
7193b9a000 fix:修复前端this undefined导致的无法选课问题 2026-01-05 15:50:51 +08:00
Cai
042f6ee99e restore some file 2025-12-31 10:45:42 +08:00
CakeCN
8fd88b0088 add jquery to dashboard 2025-12-31 10:35:25 +08:00
CakeCN
319b111485 pro config 2025-12-29 21:29:54 +08:00
CakeCN
f060a17f6a 生产服 .com 2025-12-29 21:10:49 +08:00
CakeCN
ecdc2e320f Merge branch 'main' of https://hsamooc.com/git/CakeCN/hsa 2025-12-29 20:53:22 +08:00
CakeCN
aa9f39ca18 Merge branch 'main' of https://hsamooc.com/git/CakeCN/hsa 2025-12-29 20:52:54 +08:00
CakeCN
f444e86136 许多更新
Merge branch 'test'
2025-12-29 20:52:03 +08:00
68493d6a62 add:教师端新增删除课程,增加并更换footer-brief模板 2025-12-25 15:52:29 +08:00
f3c30a80a3 fix:教师端新增课程卡片中选择从课程创建的渲染错误问题(传递的是materials而不是courses) 2025-12-25 15:17:12 +08:00
f09d82571f fix:教师端新增课程的卡片自适应布局 2025-12-25 14:33:52 +08:00
CakeCN
3c8821b225 Merge branch 'test' 2025-12-13 23:51:11 +08:00
CakeCN
ae9f7b84f4 Merge branch 'test'
修复progress Bar和提示美化
2025-12-12 16:59:58 +08:00
CakeCN
fc7c6708b4 Merge branch 'test' 2025-12-11 16:20:44 +08:00
CakeCN
a041650a54 删除app back 2025-12-11 15:00:56 +08:00
CakeCN
ad7fd04f19 add .gitattributes 2025-12-11 14:19:14 +08:00
20 changed files with 949 additions and 1167 deletions

1
Html/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
Html/config.ini merge=ours

File diff suppressed because one or more lines are too long

View File

@@ -1,480 +0,0 @@
from bootstrap import bootstrap_paths
bootstrap_paths()
from functools import wraps
from flask import Flask, redirect, session, request, jsonify, render_template, send_from_directory, url_for
from flask_cors import CORS
from flask_socketio import SocketIO, join_room, emit, Namespace
import markdown
import os
import uuid
import shutil
import sys
import json
from apps.auth.decorators import require_role
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, parent_dir)
student_workspace_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../study'))
current_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))
sys.path.insert(0, current_dir)
from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager
import configparser
from db.user_list import UserList
from db.user import User, load_user_from_json, create_user_json
from db.course_list import CourseList
from db.course import Course, load_course_from_json
GLOBAL_CONFIG = configparser.ConfigParser()
GLOBAL_CONFIG.read('config.ini')
VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url']
USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir']
COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir']
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*",ping_timeout=60, ping_interval=5)
# socketio = SocketIO(app, cors_allowed_origins="http://localhost:9888") # 设置跨域支持
import logging
app.secret_key = 'cakebaker'
app.logger.setLevel(logging.DEBUG)
# 配置 CORS
CORS(app, resources={r"/*": {"origins": VSCODE_WEB_URL},}, supports_credentials=True)
userid_recorder = {} # user_id&path -> session['user_id']
'''
Backboard
'''
from backboardManager import BackBoardManager, Backboard
backboard_manager = BackBoardManager()
uuid2username = {}
username2uuid = {}
# 定义命名空间:用于和 VSCode 插件交流
class VSCodeNamespace(Namespace):
def on_login(self,data):
print("VSCode client connected")
print(data)
dataconfig = data['config']
user_id = dataconfig.get('user_id')
path = dataconfig.get('path')
course_id = dataconfig.get('course_id')
lesson_id = dataconfig.get('chapter_id')
print(f"User {user_id} connected with path: {path}")
useruuid = username2uuid[user_id]
join_room(useruuid, namespace='/vscode')
if backboard_manager.get_backboard(useruuid) == None:
backboard_manager.add_backboard(useruuid,user_id, course_id, lesson_id, path)
def on_message(self, data):
print(f"Received from VSCode client: {data}")
dataconfig = data['config']
realtime_response(dataconfig, data)
# emit('response', {'message': 'Config data received and connection established'})
def on_disconnect(self, data):
print("VSCode client disconnected")
print("Disconnect reason:"+str(data))
def realtime_response(config, realtime_action):
user_id = config['user_id']
folder_path = config['path']
useruuid = username2uuid[user_id]
bb = backboard_manager.get_backboard(useruuid)
assert type(bb) == Backboard
bb.add_history(realtime_action)
if realtime_action['type'] == 'workspaceFolders':
bb.file_tree = realtime_action['fileTree']
if realtime_action['type'] == 'activeFile':
file_path = realtime_action['filePath']
assert type(file_path) == str
with open(f"{file_path}") as f:
bb.active_file_content = f.read()
if realtime_action['type'] == 'paste':
file_path = realtime_action['filePath']
assert type(file_path) == str
bb.pasted_file_path = file_path
bb.pasted_content = realtime_action['content']
bb.active_file_path = file_path
with open(f"{file_path}") as f:
bb.active_file_content = f.read()
if realtime_action['type'] == 'fileEdit':
file_path = realtime_action['filePath']
assert type(file_path) == str
bb.active_file_path = file_path
with open(f"{file_path}") as f:
bb.active_file_content = f.read()
print("config"+str(config))
print("realtime_action"+str(realtime_action))
pass
'''
Agent and Chat
'''
agent_manager = AgentManager(app = app, socketio = socketio)
user_threads = {}
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
# 初始化线程池
executor = ThreadPoolExecutor(max_workers=10)
class AgentNamespace(Namespace):
def on_login(self, data):
data = json.loads(data)
user = data['username']
course_id = data['course_id']
chapter_id = data['chapter_id']
user_uuid = username2uuid[user]
session['user_id'] = user_uuid
print(f'User connected with session user_id: {user_uuid}')
# 从markdown、markdown_prompts、score_prompts中各读取数据folder.md并进行new_agent
with open(f'books/markdown/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fmd,\
open(f'books/markdown_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8')as fmdp,\
open(f'books/score_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fsp:
markdown = fmd.read()
markdown_prompts = fmdp.read()
score_prompts = fsp.read()
user_uuid, agent = agent_manager.new_agent(course_id, chapter_id, markdown, markdown_prompts, score_prompts, id=user_uuid,
root_path=f'../../study/{user}/{course_id}/{chapter_id}')
print(user_uuid)
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
backboard_manager.add_backboard(user_uuid, user, course_id, chapter_id, root_path=f'../../study/{user}/{course_id}/{chapter_id}')
def on_language(self, language):
id = session.get('user_id')
agent_manager.change_language(id, language)
def on_message(self, data):
print(f"Message from client: {data}")
id = session.get('user_id')
if (type(data)==str):
data = json.loads(data)
print(id)
if data['type'] == 'text':
res = agent_manager.invoke(id, data['data'], backboard_manager.get_backboard(id).get_info_prompt())
print('=*='*20)
print(res.content)
reply = f"{res.content['speak']}"
with app.app_context():
emit('message',reply, room=id, namespace='/agent')
emit('request_function',res.content['function'], room=id, namespace='/agent')
if data['type'] == 'function':
agent_manager.function_call(id, data['data'])
def on_initiative(self,data):
print("User active function call")
user_id = session['user_id']
if data['name'] == 'sample_judge':
agent_manager.sample_judge(user_id, backboard_manager.get_backboard(user_id))
if data['name'] == 'judge':
agent_manager.judge(user_id, backboard_manager.get_backboard(user_id))
def on_disconnect(self,data):
print("VSCode client disconnected")
print("Disconnect reason:"+str(data))
'''
Markdown to HTML
'''
# 配置文件路径
MARKDOWN_DIR = 'books/markdown' # 存放 markdown 文件的文件夹
STATIC_DIR = 'static' # 存放生成的 HTML 和图片资源的文件夹
IMAGE_DIR = 'image' # 图片资源相对于 markdown 的位置
@app.route('/<course_id>-<filename>-markdown', methods=['GET'])
def convert_md(course_id, filename):
md_file_path = os.path.join(MARKDOWN_DIR,course_id, f'{filename}.md')
if not os.path.exists(md_file_path):
return jsonify({'error': 'Markdown file not found'}), 404
# 读取 markdown 文件内容
with open(md_file_path, 'r', encoding='utf-8') as file:
md_content = file.read()
# 将 markdown 转换为 HTML
html_content = markdown.markdown(md_content)
# 插入 CSS 样式,限制图片最大宽度为 100%,高度自动调整
html_with_styles = f"""
<html>
<head>
<style>
img {{
max-width: 100%;
height: auto;
}}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
</head>
<body>
{html_content}
</body>
</html>
"""
# 保存 HTML 文件到 static 文件夹
html_output_path = os.path.join(STATIC_DIR, f'{filename}.html')
with open(html_output_path, 'w', encoding='utf-8') as html_file:
html_file.write(html_with_styles)
# 处理图片资源,将 image/xxx 文件夹拷贝到 static/image/xxx
image_source_dir = os.path.join(MARKDOWN_DIR,course_id, IMAGE_DIR, filename)
image_target_dir = os.path.join(STATIC_DIR, IMAGE_DIR, filename)
print(f"Copying image resources from {image_source_dir} to {image_target_dir}")
if os.path.exists(image_source_dir):
# 确保 static/image/xxx 目录存在
if not os.path.exists(image_target_dir):
os.makedirs(image_target_dir)
# 拷贝图片资源
for image_file in os.listdir(image_source_dir):
full_image_file_path = os.path.join(image_source_dir, image_file)
if os.path.isfile(full_image_file_path):
shutil.copy(full_image_file_path, image_target_dir)
return jsonify({'html_url': f'/static/{filename}.html'})
# 静态文件HTML 和图片资源)的访问
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory(STATIC_DIR, filename)
'''
Vscode
'''
@app.route('/desktop/<user_id>/<course_id>/<chapter_id>')
def desktop(user_id, course_id, chapter_id):
if 'user_id' not in session:
# return redirect(url_for('login'))
session['user_id'] = 'user_' + str(uuid.uuid4())
print("user "+ user_id + "uuid is"+ session['user_id'])
username2uuid[user_id] = session['user_id']
uuid2username[session['user_id']] = user_id
userid_recorder[user_id+'&'+course_id] = session['user_id']
# 在学习目录下创建一个名为 user_id_path 的文件夹
path_dir = os.path.join(student_workspace_root_path, user_id, course_id,chapter_id)
os.makedirs(path_dir, exist_ok=True)
# 在此文件夹内部创建一个.config文件并写入 user_id=user_id\n path=path
config_path = os.path.join(path_dir, '.config')
if GLOBAL_CONFIG['VSCODE_WEB_PATH']['is_wsl']: # 如果是在 WSL 中运行,则将路径转换为 WSL 路径
path_dir = path_dir.replace('\\', '/')
path_dir = path_dir.replace(GLOBAL_CONFIG['VSCODE_WEB_PATH']['windows_path'] , GLOBAL_CONFIG['VSCODE_WEB_PATH']['wsl_path'])
with open(config_path, 'w', encoding='utf-8') as f:
tmpd = {'user_id': user_id, 'course_id': course_id, 'chapter_id': chapter_id, 'path': path_dir}
json.dump(tmpd, f)
print('---------------------')
print(config_path)
return render_template('desktop.html', vscode_web_url=VSCODE_WEB_URL, user_id=user_id, course_id=course_id, chapter_id=chapter_id, workspace_path=path_dir)
@app.route('/desktop_nouser/<course_id>/<chapter_id>')
def desktop_nouser(course_id, chapter_id):
if 'user_id' not in session:
return redirect(url_for('login'))
user_id = session['user_id']
username = uuid2username[user_id]
return redirect(url_for('desktop', user_id=username, course_id=course_id, chapter_id=chapter_id))
@app.route('/vscode_data', methods=['POST'])
def vscode_data():
data = request.json
config = data['config']
realtime_response(config,data)
print(f"Received data from VSCode: {data}")
return jsonify({"status": "success", "received": data})
'''
Login
'''
users_list = UserList()
course_list = CourseList()
@app.route('/register')
def register():
return render_template('register.html')
@app.route('/register_teacher')
def register_teacher():
return render_template('register_teacher.html')
@app.route('/register_teacher_post', methods=['POST'])
def register_teacher_post():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if users_list.has_user(username) is not None:
users_list.add_user(username, password, teacher=True)
create_user_json(username, USER_DATA_DIR)
return jsonify({'success': True, 'message': '注册成功'})
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
@app.route('/register_post', methods=['POST'])
def register_post():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if users_list.has_user(username) is not None:
users_list.add_user(username, password)
create_user_json(username, USER_DATA_DIR)
return jsonify({'success': True, 'message': '注册成功'})
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
@app.route('/login')
def login():
return render_template('login.html')
if 'user_id' not in session:
return render_template('login.html')
else:
return redirect(url_for('/'))
@app.route('/login_post', methods=['POST'])
def login_post():
# 获取请求的 JSON 数据
data = request.get_json()
username = data.get('username')
password = data.get('password')
pswd = users_list.get_user_pswd(username)
if pswd is None:
return jsonify({'success': False, 'message': '用户名或密码错误'})
if pswd != password:
return jsonify({'success': False, 'message': '用户名或密码错误'})
session['user_id'] = 'user_' + str(uuid.uuid4())
print("user "+ username + "uuid is"+ session['user_id'])
username2uuid[username] = session['user_id']
uuid2username[session['user_id']] = username
return jsonify({'success': True, 'message': '登录成功'})
@app.route('/login_teacher_post', methods=['POST'])
def login_teacher_post():
data = request.get_json()
username = data.get('username')
password = data.get('password')
pswd = users_list.get_user_pswd(username)
is_teacher = users_list.get_user_is_teacher(username)
if is_teacher is None or is_teacher == False:
return jsonify({'success': False, 'message': '用户名不存在或非教师账号'})
if pswd is None:
return jsonify({'success': False, 'message': '用户名或密码错误'})
if pswd != password:
return jsonify({'success': False, 'message': '用户名或密码错误'})
session['user_id'] = 'user_' + str(uuid.uuid4())
print("user "+ username + "uuid is"+ session['user_id'])
username2uuid[username] = session['user_id']
uuid2username[session['user_id']] = username
return jsonify({'success': True, 'message': '登录成功'})
@app.route('/teacherboard')
@require_role(roles="teacher")
def teacherboard():
return render_template('teacherboard.html')
@app.route('/logout')
def logout():
session.pop('user_id', None)
return redirect(url_for('login'))
@app.route('/get_session')
def get_session():
user_session = session.get('user_id', 'default_session')
return jsonify({"session": user_session})
'''
DashBoard
'''
user_id2UserClass = {}
@app.route('/dashboard')
@require_role
def dashboard():
user_id = session['user_id']
username = uuid2username[user_id]
if (user_id not in user_id2UserClass):
user_id2UserClass[user_id] = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
user_course_data = []
for course_id in user_id2UserClass[user_id].select_course:
course_brief_info = course_list.get_course_brief_info(course_id, load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR))
user_course_data.append(course_brief_info)
return render_template('dashboard.html',user_data = user_id2UserClass[user_id].to_json_without_dialog(), user_course_data = json.dumps(user_course_data))
'''
Course
'''
@app.route('/course/<course_id>')
@require_role
def course(course_id):
c = load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR)
return render_template('course.html', course_id=course_id, course_data=c)
@app.route('/select_course', methods=['POST'])
@require_role
def select_course():
user = user_id2UserClass[session['user_id']]
data = request.get_json()
course_id = data.get('course_id')
if course_id:
user.select_new_course(course_id, load_course_from_json(course_id, COURSE_DATA_DIR))
return jsonify({'success': True, 'message': '课程选择成功'})
@app.route('/')
@require_role
def home_index():
selected_courses=[]
if ('user_id' in session):
if (session['user_id'] not in user_id2UserClass): return redirect(url_for('login'))
user = user_id2UserClass[session['user_id']]
for course_id in user.select_course:
selected_courses.append(course_id)
return render_template('index.html', courses_data = course_list, selected_courses =selected_courses)
# 一些app辅助函数主要提供给Agent与数据库的交互能力
class MyFunction:
def save_chapter_memory(self, id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal):
print("-=-=-=-=-=-=-=-")
print(id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
username = uuid2username[id]
u = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
u.save_chapter_memory(course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
app.my_function = MyFunction()
if __name__ == '__main__':
socketio.on_namespace(VSCodeNamespace('/vscode'))
socketio.on_namespace(AgentNamespace('/agent'))
socketio.run(app, host='0.0.0.0', port=5551, debug=False,allow_unsafe_werkzeug=True)

View File

@@ -60,6 +60,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer; cursor: pointer;
position: relative; /* 添加相对定位 */
} }
.course-image { .course-image {
@@ -81,6 +82,27 @@
.course-description { .course-description {
font-size: 14px; font-size: 14px;
color: #555; color: #555;
margin-bottom: 10px;
display: -webkit-box; /* 使用-webkit-box实现文本截断 */
-webkit-line-clamp: 3; /* 限制显示3行 */
-webkit-box-orient: vertical; /* 垂直排列 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 用省略号表示截断 */
}
/* 课程日期信息 */
.course-date {
position: absolute; /* 绝对定位到底部 */
bottom: 10px; /* 距离底部10px */
left: 0; /* 左对齐 */
right: 0; /* 右对齐 */
padding: 10px; /* 内边距 */
background-color: white; /* 白色背景 */
border-radius: 5px; /* 圆角 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影 */
font-size: 12px; /* 字体大小 */
color: #666; /* 字体颜色 */
line-height: 1.5; /* 行高 */
} }
/* 课程目录弹出框 */ /* 课程目录弹出框 */
@@ -159,11 +181,14 @@
/* 浮窗内容 */ /* 浮窗内容 */
.modal-content { .modal-content {
background-color: #fff; background-color: #fff;
margin: 15% auto; margin: 10% auto; /* 修改为10%以缩小顶部距离 */
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
width: 40%; width: 80%; /* 修改为80%以适应小屏幕 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); max-width: 600px; /* 设置最大宽度 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0 1);
overflow-y: auto; /* 添加垂直滚动条 */
max-height: 80vh; /* 设置最大高度 */
} }
/* 按钮样式 */ /* 按钮样式 */
@@ -205,7 +230,34 @@ label {
margin-top: 20px; /* 上方间距 */ margin-top: 20px; /* 上方间距 */
} }
/* 选择课程或新建课程的下拉框 */
.course-selection-container {
margin: 10px 0;
font-size: 16px;
}
#course-selection {
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; /* 添加过渡效果 */
}
/* 鼠标悬停时下拉框的效果 */
#course-selection:hover {
border-color: #4CAF50; /* 悬停时的边框颜色 */
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5); /* 悬停时的阴影效果 */
}
/* 下拉框选中项的样式 */
#course-selection option {
padding: 10px;
font-size: 16px;
}
/* 添加课程按钮样式 */ /* 添加课程按钮样式 */
.add-course-button { .add-course-button {
@@ -433,6 +485,20 @@ label[for="course-description"] {
cursor: pointer; cursor: pointer;
} }
.delete-course-btn {
background-color: #e74c3c;
color: white;
padding: 10px;
border-radius: 5px;
cursor: pointer;
width: 100%;
margin-top: 10px;
}
.delete-course-btn:hover {
background-color: #c0392b;
}
/* 编辑弹窗 */ /* 编辑弹窗 */
.edit-modal { .edit-modal {
display: none; display: none;

View File

@@ -1,41 +1,53 @@
function show_books(courses_data, user_selected_courses){ function show_books(courses_data, user_selected_courses) {
console.log(courses_data) console.log(courses_data)
console.log(user_selected_courses) console.log(user_selected_courses)
} }
function show_course_details(course_id){ function show_course_details(course_id) {
window.location.href = '/course/' + course_id window.location.href = '/course/' + course_id
} }
function on_click_select_course(event){ function on_click_select_course(event) {
event.stopPropagation(); // 阻止事件冒泡 event.stopPropagation(); // 阻止事件冒泡
const courseId = event.currentTarget.getAttribute('data-course-id'); const button = event.currentTarget;
if (event.currentTarget.classList.contains("selected-button")) {alert('课程已选择'); return;} const courseId = button.getAttribute('data-course-id');
if (!courseId) { alert('缺少课程ID'); return; }
if (button.classList.contains("selected-button")) { alert('课程已选择'); return; }
if (button.disabled) { return; }
button.disabled = true;
fetch(`/select_course`, { fetch(`/select_course`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
}, },
body: JSON.stringify({ course_id: courseId }) body: JSON.stringify({ course_id: courseId })
}) })
.then(response => response.json()) .then(async (response) => {
.then(data => { let data = null;
if (data.success) { try {
// 更新按钮样式或显示消息 data = await response.json();
this.classList.remove('select-button'); } catch (_) {
this.classList.add('selected-button'); }
this.textContent = '已选择'; if (!response.ok) {
throw new Error(data?.message || `请求失败(${response.status})`);
}
return data;
})
.then((data) => {
if (!data?.success) {
throw new Error(data?.message || '选择课程失败');
}
button.classList.remove('select-button');
button.classList.add('selected-button');
button.textContent = '已选课';
alert('选择课程成功'); alert('选择课程成功');
} else { })
alert('选择课程失败'); .catch(error => {
} console.error('Error:', error);
}) alert(error?.message || '选择课程失败');
.catch(error => { button.disabled = false;
console.error('Error:', error); });
alert('选择课程失败');
});
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -292,13 +292,50 @@ function closeAddCourseModal() {
document.getElementById('add-course-modal').style.display = 'none'; document.getElementById('add-course-modal').style.display = 'none';
} }
// 显示删除课程确认弹窗
function showDeleteCourseConfirmation() {
if (!material_id) {
alert('无法删除课程未找到课程ID');
return;
}
if (confirm('确定要删除这个课程吗?此操作无法撤销!')) {
deleteCourse(material_id);
}
}
// 删除课程
function deleteCourse(courseId) {
fetch(`/materials/${courseId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert(data.message);
if (data.success) {
// 关闭侧边栏并刷新页面
closeCourseDetails();
location.reload();
}
}
})
.catch(error => {
console.error('删除课程时出错:', error);
alert('删除课程失败,请稍后再试!');
});
}
function onTeacherboardPageLoad(){ function onTeacherboardPageLoad(){
// 提交表单处理 // 提交表单处理
document.getElementById('add-course-form').addEventListener('submit', function(event) { document.getElementById('add-course-form').addEventListener('submit', function(event) {
event.preventDefault(); event.preventDefault();
const courseName = document.getElementById('course-name').value; const courseName = document.getElementById('course-name').value;
const courseSelection = document.getElementById('course-selection').value; const courseSelection = document.getElementById('course-selection').value; // 获取选择值
const coverImage = document.getElementById('cover-preview').src; const coverImage = document.getElementById('cover-preview').src;
const courseDescription = document.getElementById('course-description').value; const courseDescription = document.getElementById('course-description').value;
// 构建发送的数据 // 构建发送的数据
@@ -312,7 +349,7 @@ document.getElementById('add-course-form').addEventListener('submit', function(e
// 如果选择了已有课程,可以通过 courseSelection 传递课程ID根据需要修改 // 如果选择了已有课程,可以通过 courseSelection 传递课程ID根据需要修改
if (courseSelection !== 'new') { if (courseSelection !== 'new') {
// 如果是修改已有课程,设置相关的章节信息或其他数据 // 如果是修改已有课程,设置相关的章节信息或其他数据
data.chapters = ["Chapter 1", "Chapter 2"]; // 示例章节信息 data.chapters = ["Chapter 1", "Chapter 2"];
} }
// 发送 POST 请求到 /create_material // 发送 POST 请求到 /create_material

View File

@@ -46,7 +46,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
</body> </body>
<script> <script>
var course_id = "{{course_id}}"; var course_id = "{{course_id}}";

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华实学伴 - 专业的在线学习平台</title> <title>华实学伴 - 专业的在线学习平台</title>
<script src="/static/cdnback/jquery.min.js"></script>
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet"> <link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/cdnback/all.min.css"> <link rel="stylesheet" href="/static/cdnback/all.min.css">
<link rel="stylesheet" href="/static/css/dashboard.css"> <link rel="stylesheet" href="/static/css/dashboard.css">
@@ -13,7 +14,6 @@
</head> </head>
<body> <body>
{% include 'navbar.html' %} <!-- 引入navbar.html --> {% include 'navbar.html' %} <!-- 引入navbar.html -->
{% include 'learning-path.html' %} <!-- 引入learning-path.html -->
<section class="search-section"> <section class="search-section">
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
@@ -81,7 +81,7 @@
</div> </div>
</div> </div>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
<script src="/static/js/dashboard.js"></script> <script src="/static/js/dashboard.js"></script>
<script> <script>

View File

@@ -0,0 +1,11 @@
<link rel="stylesheet" href="/static/css/footer.css">
<footer class="hs-footer">
<div class="footer-main">
<div class="footer-bottom">
<div class="footer-bottom-right">
<span>© 2025 华实学伴. 保留所有权利</span>
<a href="https://beian.miit.gov.cn" target="_blank">沪ICP备2025142149号</a>
</div>
</div>
</footer>

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>课程选择主页</title> <title>课程选择主页</title>
<script src="/static/cdnback/jquery.min.js"></script> <script src="/static/cdnback/jquery.min.js"></script>
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet"> <link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
@@ -12,6 +13,7 @@
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script> <script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css"> <link rel="stylesheet" href="/static/css/index.css">
</head> </head>
<body> <body>
{% include 'navbar.html' %} {% include 'navbar.html' %}
@@ -38,7 +40,7 @@
<h2>课程列表</h2> <h2>课程列表</h2>
<div class="course-cards" id="course-cards"> <div class="course-cards" id="course-cards">
{% for course in courses_data %} {% for course in courses_data %}
<!-- <div class="course-card" onclick="show_course_details('{{ course._id }}')"> <!-- <div class="course-card" onclick="show_course_details('{{ course._id }}')">
<img src="{{ course.image_url }}" alt="{{ course.course_name }}"> <img src="{{ course.image_url }}" alt="{{ course.course_name }}">
<h3>{{ course.name }}</h3> <h3>{{ course.name }}</h3>
<p>{{ course.description }}</p> <p>{{ course.description }}</p>
@@ -49,10 +51,9 @@
{% endif %} {% endif %}
</div> --> </div> -->
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')"> <div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')">
<img src="{{course.image_url}}" <img src="{{course.image_url}}" class="course-img" alt="课程封面">
class="course-img" alt="课程封面">
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<span class="course-category">算法设计</span> <span class="course-category">算法设计</span>
<h5 class="course-title">{{course.name}}</h5> <h5 class="course-title">{{course.name}}</h5>
@@ -62,11 +63,15 @@
<span class="course-rating"><i class="fas fa-star"></i> 4.7</span> <span class="course-rating"><i class="fas fa-star"></i> 4.7</span>
</div> </div>
<div class="mt-auto d-grid gap-2 d-md-flex"> <div class="mt-auto d-grid gap-2 d-md-flex">
<button class="btn btn-view flex-fill" onclick="btn_view(event,'{{course._id}}')"><i class="far fa-eye me-1"></i> 课程</button> <button class="btn btn-view flex-fill" onclick="btn_view(event,'{{course._id}}')"><i
class="far fa-eye me-1"></i> 课程</button>
{% if course._id in selected_courses %} {% if course._id in selected_courses %}
<button class="btn btn-process flex-fill selected-button" data-course-id="{{course._id}}">已选课</button> <button class="btn btn-process flex-fill selected-button"
data-course-id="{{course._id}}">已选课</button>
{% else %} {% else %}
<button class="btn btn-process flex-fill select-button" onclick="on_click_select_course(event)" data-course-id="{{course._id}}">选择课程</button> <button class="btn btn-process flex-fill select-button"
onclick="on_click_select_course(event)"
data-course-id="{{course._id}}">选择课程</button>
{% endif %} {% endif %}
</div> </div>
@@ -74,13 +79,13 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</main> </main>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
</body> </body>
<script> <script>
window.appData = { window.appData = {
@@ -88,5 +93,6 @@
} }
</script> </script>
<script src="/static/js/index.js"></script> <script src="/static/js/index.js"></script>
</html> <script src="/static/js/dashboard.js"></script>
</html>

View File

@@ -65,7 +65,7 @@
</div> </div>
</ul> </ul>
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button> <button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
<button class="delete-course-btn" onclick="showDeleteCourseConfirmation()">删除课程</button>
</div> </div>
<!-- 编辑章节或课时弹窗 --> <!-- 编辑章节或课时弹窗 -->
@@ -90,9 +90,9 @@
<div class="cover-image-container"> <div class="cover-image-container">
<div id="cover-image" class="cover-image-placeholder"> <div id="cover-image" class="cover-image-placeholder">
<span class="plus-sign">+</span> <span class="plus-sign">+</span>
<input type="file" id="cover-image-input" style="display: none;" accept="image/*" /> <input type="file" id="cover-image-input" style="display: none;" accept="image/*" />
</div> </div>
<img id="cover-preview" src="" alt="封面预览" style="display: none;" /> <img id="cover-preview" src="" alt="封面预览" style="display: none;" />
</div> </div>
<form id="add-course-form"> <form id="add-course-form">
@@ -100,8 +100,8 @@
<label for="course-selection">从课程创建或新建课程:</label> <label for="course-selection">从课程创建或新建课程:</label>
<select id="course-selection"> <select id="course-selection">
<option value="new">新建课程</option> <option value="new">新建课程</option>
{% for course in courses %} {% for course in materials %}
<option value="{{ course.id }}">{{ course.name }}</option> <option value="{{ course._id }}">{{ course.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<br><br> <br><br>
@@ -121,6 +121,6 @@
<script src="/static/js/course_current.js"></script> <script src="/static/js/course_current.js"></script>
<script src="/static/js/teacherboard.js"></script> <script src="/static/js/teacherboard.js"></script>
<script src="/static/js/teacher_course_setting.js"></script> <script src="/static/js/teacher_course_setting.js"></script>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
</body> </body>
</html> </html>

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, request, jsonify, current_app, session, render_template, session from flask import Blueprint, request, jsonify, current_app, session, render_template, session
from bson import ObjectId
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 ..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 ..auth.decorators import require_role
from ..services.cos_service import upload_file from ..services.cos_service import upload_file
@@ -37,24 +38,60 @@ def create_new_material():
material_id = create_material(teacher_id, material_name, description, chapters, image_url) material_id = create_material(teacher_id, material_name, description, chapters, image_url)
return jsonify({"message": "教材创建成功", "material_id": material_id}), 201 return jsonify({"message": "教材创建成功", "material_id": material_id}), 201
@bp.route('/materials/<material_id>', methods=['GET']) @bp.route('/materials/<material_id>', methods=['GET', 'PUT', 'DELETE'])
@require_role(roles="teacher") @require_role(roles="teacher")
def get_material(material_id): def handle_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
if request.method == 'GET':
@bp.route('/materials/<material_id>', methods=['PUT']) if material_id is None:
@require_role(roles="teacher") return jsonify({"message": "教材名称不能为空"}), 400
def update_existing_material(material_id): material = load_material(material_id)
data = request.get_json() print(material)
chapters = data.get('chapters') return jsonify(material.model_dump()), 200
if update_material(material_id, chapters):
return jsonify({"message": "教材更新成功"}), 200 elif request.method == 'PUT':
return jsonify({"message": "教材未找到"}), 404 data = request.get_json()
chapters = data.get('chapters')
if update_material(material_id, chapters):
return jsonify({"message": "教材更新成功"}), 200
else:
return jsonify({"message": "教材更新失败"}), 500
elif request.method == 'DELETE':
# 获取当前教师信息
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
# 检查材料是否存在,并且是否为当前教师创建
mongo = current_app.extensions["mongo"]
print(f"尝试删除课程ID: {material_id}, 当前教师ID: {teacher_id}")
try:
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
print(f"找到的材料: {material}")
if not material:
return jsonify({"message": "课程不存在", "success": False}), 404
# 验证是否为当前教师创建的课程
if material.get('teacher_id') != teacher_id:
return jsonify({"message": "权限不足,无法删除他人课程", "success": False}), 403
except Exception as e:
print(f"查询材料时出错: {e}")
return jsonify({"message": f"查询课程时出错: {str(e)}", "success": False}), 500
# 执行删除操作
try:
result = mongo.db.materials.delete_one({'_id': ObjectId(material_id), 'teacher_id': teacher_id})
print(f"删除操作结果: {result.deleted_count}")
except Exception as e:
print(f"删除材料时出错: {e}")
return jsonify({"message": f"删除课程时出错: {str(e)}", "success": False}), 500
if result.deleted_count > 0:
return jsonify({"message": "课程删除成功", "success": True}), 200
else:
return jsonify({"message": "删除失败", "success": False}), 500
@bp.route('/materials/addchapter/<material_id>', methods=['POST']) @bp.route('/materials/addchapter/<material_id>', methods=['POST'])
@require_role(roles="teacher") @require_role(roles="teacher")

View File

@@ -5,7 +5,7 @@ api_key = sk-aMpnWklN2IbsK44d1kNpy6YOP9bk1pdPJjFeEmbb0a5ytEFf
model=gpt-4.1-nano model=gpt-4.1-nano
[VSCODE_WEB] [VSCODE_WEB]
url = https://hsamooc.cn url = https://hsamooc.com
[CODE_LIKE] [CODE_LIKE]
url = /vsc-like url = /vsc-like
#http://asengine.net:8282 #http://asengine.net:8282
@@ -27,6 +27,6 @@ bucket = hsamooc-cdn-1374354408
region = ap-guangzhou region = ap-guangzhou
[ASE_ENGINE] [ASE_ENGINE]
url = https://test.asengine.net url = https://asengine.net
namespace = /socket.io/afe58a9e4c03e10fae6fae5985e8adde23a49635 namespace = /socket.io/afe58a9e4c03e10fae6fae5985e8adde23a49635
url_token = afe58a9e4c03e10fae6fae5985e8adde23a49635 url_token = afe58a9e4c03e10fae6fae5985e8adde23a49635

View File

@@ -0,0 +1 @@
{"username": "_10235101560"}

View File

@@ -0,0 +1 @@
{"username": "shanks"}

View File

@@ -0,0 +1 @@
{"username": "ts88"}

View File

@@ -0,0 +1 @@
{"username": "xuans"}

View File

@@ -0,0 +1 @@
{"username": "xuans_"}

69
et --hard HEAD@{1} Normal file
View File

@@ -0,0 +1,69 @@
68493d6a (HEAD -> main, origin/main) HEAD@{0}: pull origin main: Fast-forward
0852e121 HEAD@{1}: reset: moving to HEAD
0852e121 HEAD@{2}: commit: cdn fix more
7dc4714a HEAD@{3}: commit: index style
9dc81b14 (origin/feature/paste_detecte_new, feature/paste_detecte_new) HEAD@{4}: merge feature/paste_detecte_new: Fast-forward
7862647b HEAD@{5}: checkout: moving from feature/paste_detecte_new to main
9dc81b14 (origin/feature/paste_detecte_new, feature/paste_detecte_new) HEAD@{6}: commit (merge): Merge branch 'main' into feature/paste_detecte_new
0cf1896b HEAD@{7}: checkout: moving from main to feature/paste_detecte_new
7862647b HEAD@{8}: commit: style fix
6b9c8e9e HEAD@{9}: commit: cdn change to local and use markdown and style change
33844786 HEAD@{10}: commit: use markdown to edit
ce88ae64 HEAD@{11}: commit: use queue to allow send vscode_ws when it is none
6c38e906 HEAD@{12}: pull: Fast-forward
9759c93a HEAD@{13}: commit: fix funciton response
ecf19f49 HEAD@{14}: commit: fix function double promlem
f5d50772 HEAD@{15}: commit: function call can be expire
4a390adb HEAD@{16}: commit: fix style bug
960d1f1d HEAD@{17}: commit: style fix
c73ea447 HEAD@{18}: commit: fix index
33bf569f HEAD@{19}: commit: fix nextchapter bug
b921bda4 HEAD@{20}: commit: auto get domain
9921848d HEAD@{21}: commit: remove prefix of http url
4d81aef8 HEAD@{22}: commit (merge): Merge branch 'feature/paste_deteced'
8fe5ee16 HEAD@{23}: checkout: moving from feature/paste_deteced to main
9ce212e9 (origin/feature/paste_deteced, feature/paste_deteced) HEAD@{24}: checkout: moving from main to feature/paste_deteced
8fe5ee16 HEAD@{25}: commit (merge): 准备合并main
908d5915 HEAD@{26}: checkout: moving from syt to main
57d8ac32 (origin/syt, syt) HEAD@{27}: checkout: moving from main to syt
908d5915 HEAD@{28}: commit: ready
02b12791 HEAD@{29}: checkout: moving from syt to main
57d8ac32 (origin/syt, syt) HEAD@{30}: checkout: moving from main to syt
02b12791 HEAD@{31}: pull: Fast-forward
7d861e34 (origin/zrz) HEAD@{32}: checkout: moving from syt to main
57d8ac32 (origin/syt, syt) HEAD@{33}: commit: first
0196f37e HEAD@{34}: pull: Merge made by the 'ort' strategy.
57eb1197 HEAD@{35}: commit: ok
382f2006 HEAD@{36}: checkout: moving from main to syt
7d861e34 (origin/zrz) HEAD@{37}: commit (merge): Merge branch 'main' of https://gitee.com/CakeCN/code-agent
5f8f6c94 HEAD@{38}: commit: many ok
e24aa12e HEAD@{39}: commit: ready to process
6766ca85 HEAD@{40}: commit (merge): Merge branch 'feature/voice_send'
3c9c7553 HEAD@{41}: checkout: moving from feature/voice_send to main
58a95e9f (origin/feature/voice_send, feature/voice_send) HEAD@{42}: commit: ready merge to main
79125ffc HEAD@{43}: commit: change .cn to .com
2f9bc7df HEAD@{44}: checkout: moving from main to feature/voice_send
3c9c7553 HEAD@{45}: reset: moving to HEAD
3c9c7553 HEAD@{46}: checkout: moving from feature/voice_send to main
2f9bc7df HEAD@{47}: pull: Fast-forward
504c5cae (0v) HEAD@{48}: checkout: moving from main to feature/voice_send
3c9c7553 HEAD@{49}: commit: cn/com allow all
7122c3be HEAD@{50}: commit (merge): fix url
e07c41fb HEAD@{51}: commit: fix code-server reboot problem(close debug)
821e20c9 HEAD@{52}: commit: clear double iframe more safe
053584a7 HEAD@{53}: checkout: moving from feature/voice_send to main
504c5cae (0v) HEAD@{54}: checkout: moving from main to feature/voice_send
053584a7 HEAD@{55}: commit: clear iframe double
10481b0b HEAD@{56}: commit: fix permisson bug
dc0eeaa9 HEAD@{57}: commit: add hint
a24df620 HEAD@{58}: commit: fix some bug
3ad36c29 HEAD@{59}: commit: 25/09/24 10:00~11:30 停机维护
07cf2363 HEAD@{60}: commit: fix many bug
6aac4200 HEAD@{61}: commit: try hello message
271672c9 HEAD@{62}: commit: check to code-server-like
252c426c HEAD@{63}: pull: Merge made by the 'ort' strategy.
588fc3b6 HEAD@{64}: commit: ready to code-like
b1f832a2 (origin/code-like-autosave, code-like-autosave) HEAD@{65}: merge code-like-autosave: Fast-forward
24f191d5 HEAD@{66}: checkout: moving from code-like-autosave to main
b1f832a2 (origin/code-like-autosave, code-like-autosave) HEAD@{67}: checkout: moving from main to code-like-autosave
24f191d5 HEAD@{68}: clone: from https://gitee.com/CakeCN/code-agent.git