Compare commits
20 Commits
7862647b30
...
ack-client
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f7a9d104 | ||
|
|
a210f5bac3 | ||
|
|
dc9f246841 | ||
|
|
74f26c72fd | ||
|
|
aae10819ca | ||
|
|
42549bb32a | ||
|
|
93cf991779 | ||
|
|
788fbb07da | ||
|
|
6c48d79076 | ||
|
|
5e6e040753 | ||
|
|
ede89cef53 | ||
|
|
0fd548edcd | ||
| 553a7724b3 | |||
| 0852e12118 | |||
| 7dc4714ae9 | |||
| 9dc81b1489 | |||
| 0cf1896b5b | |||
| a45e5f6f37 | |||
| ad2de51a55 | |||
| 293cc24ac5 |
@@ -48,4 +48,5 @@ class Config:
|
||||
}
|
||||
|
||||
ASE_ENGINE_URL = GLOBAL_CONFIG['ASE_ENGINE']['url']
|
||||
ASE_ENGINE_URL_TOKEN = GLOBAL_CONFIG['ASE_ENGINE']['url_token']
|
||||
ASE_ENGINE_NAMESPACE = GLOBAL_CONFIG['ASE_ENGINE']['namespace']
|
||||
124
Html/apps/extension_ase/ase_client/ACK_SUPPORT_GUIDE.md
Normal file
124
Html/apps/extension_ase/ase_client/ACK_SUPPORT_GUIDE.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ACK机制支持指南
|
||||
|
||||
## 概述
|
||||
|
||||
客户端已实现基于emit机制的ACK确认功能,允许客户端在发送消息后等待服务器确认,从而支持同步和异步两种通信模式。
|
||||
|
||||
## 客户端改动
|
||||
|
||||
### 1. 新增参数
|
||||
|
||||
客户端的三个主要发送方法 `send_text`、`send_voice` 和 `send_image` 都新增了以下参数:
|
||||
|
||||
- `wait_for_ack: bool = False` - 是否等待服务器ACK确认
|
||||
- `timeout: int = 5` - 等待ACK的超时时间(秒)
|
||||
|
||||
### 2. 消息格式变化
|
||||
|
||||
客户端发送的消息中新增了 `ack_id` 字段,格式为 `{id}_{timestamp}`,其中:
|
||||
- `id` 是客户端ID
|
||||
- `timestamp` 是当前时间的格式化字符串(精确到微秒)
|
||||
|
||||
### 3. ACK事件
|
||||
|
||||
客户端监听 `ack` 事件,用于接收服务器的确认消息。
|
||||
|
||||
## 后端WebSocket服务器需要的支持
|
||||
|
||||
### 1. 接收消息处理
|
||||
|
||||
当服务器收到客户端发送的消息时,需要:
|
||||
|
||||
1. 检查消息中是否包含 `ack_id` 字段
|
||||
2. 如果包含 `ack_id`,立即发送ACK确认消息,不需要等待后续业务逻辑执行
|
||||
3. 继续执行后续的业务逻辑
|
||||
|
||||
### 2. ACK消息格式
|
||||
|
||||
服务器需要向客户端发送 `ack` 事件,消息格式如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"ack_id": "客户端发送的ack_id值"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 发送ACK的时机
|
||||
|
||||
**非常重要**:服务器必须在收到客户端消息后**立即**发送ACK确认,而不是在业务逻辑执行完成后发送。这样可以确保客户端能够快速收到确认,继续执行后续操作。
|
||||
|
||||
### 4. 实现示例(Python Socket.IO)
|
||||
|
||||
```python
|
||||
# 服务器端示例代码
|
||||
@sio.on('text_message') # 根据实际事件名调整
|
||||
def handle_text_message(sid, data):
|
||||
# 1. 立即发送ACK确认
|
||||
if 'ack_id' in data:
|
||||
sio.emit('ack', {'ack_id': data['ack_id']}, room=sid)
|
||||
|
||||
# 2. 执行后续业务逻辑(可能耗时较长)
|
||||
# ... 业务逻辑代码 ...
|
||||
|
||||
@sio.on('voice_message') # 根据实际事件名调整
|
||||
def handle_voice_message(sid, data):
|
||||
# 1. 立即发送ACK确认
|
||||
if 'ack_id' in data:
|
||||
sio.emit('ack', {'ack_id': data['ack_id']}, room=sid)
|
||||
|
||||
# 2. 执行后续业务逻辑(可能耗时较长)
|
||||
# ... 业务逻辑代码 ...
|
||||
|
||||
@sio.on('image_message') # 根据实际事件名调整
|
||||
def handle_image_message(sid, data):
|
||||
# 1. 立即发送ACK确认
|
||||
if 'ack_id' in data:
|
||||
sio.emit('ack', {'ack_id': data['ack_id']}, room=sid)
|
||||
|
||||
# 2. 执行后续业务逻辑(可能耗时较长)
|
||||
# ... 业务逻辑代码 ...
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 客户端同步调用(等待ACK)
|
||||
|
||||
```python
|
||||
client = ASEngineClient(server_url, namespace, id)
|
||||
client.connect()
|
||||
|
||||
# 同步调用:等待ACK确认后再继续执行
|
||||
result = client.send_text('text_message', 'Hello', wait_for_ack=True, timeout=3)
|
||||
if result:
|
||||
print("消息已被服务器确认收到")
|
||||
# 执行后续操作
|
||||
else:
|
||||
print("等待ACK超时")
|
||||
```
|
||||
|
||||
### 客户端异步调用(不等待ACK)
|
||||
|
||||
```python
|
||||
client = ASEngineClient(server_url, namespace, id)
|
||||
client.connect()
|
||||
|
||||
# 异步调用:发送后立即返回,不等待ACK
|
||||
result = client.send_text('text_message', 'Hello', wait_for_ack=False)
|
||||
if result:
|
||||
print("消息已发送")
|
||||
# 执行后续操作,不需要等待ACK
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **向后兼容**:客户端的改动保持了向后兼容性,现有代码无需修改即可继续使用。
|
||||
2. **超时处理**:客户端会在指定的超时时间后停止等待ACK,并返回False。
|
||||
3. **资源清理**:客户端会在收到ACK或超时时清理相关的ACK状态,避免内存泄漏。
|
||||
4. **唯一标识**:`ack_id` 确保了每个消息的唯一性,避免ACK混淆。
|
||||
5. **性能考虑**:等待ACK会阻塞当前线程,建议仅在必要时使用同步模式。
|
||||
|
||||
## 总结
|
||||
|
||||
通过实现基于emit机制的ACK确认功能,客户端和服务器之间的通信可以灵活支持同步和异步两种模式。服务器需要做的改动非常简单,只需要在收到消息后立即发送ACK确认即可。
|
||||
|
||||
这种设计既保证了通信的可靠性,又不会影响业务逻辑的执行效率,是一种高效且可靠的通信方式。
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Callable, Any, Optional
|
||||
from functools import partial
|
||||
from threading import Event
|
||||
|
||||
class ASEngineClient:
|
||||
def __init__(self, server_url: str, namespace: str, id:str):
|
||||
@@ -29,8 +30,9 @@ class ASEngineClient:
|
||||
self._on_connect_callback_entry=None
|
||||
self._on_connect_callback_initData=None
|
||||
|
||||
|
||||
|
||||
# ACK相关
|
||||
self.ack_events: Dict[str, Event] = {} # 存储ack_id和对应的Event对象
|
||||
|
||||
# 注册基础事件处理函数
|
||||
self._register_base_events()
|
||||
|
||||
@@ -62,6 +64,15 @@ class ASEngineClient:
|
||||
@self.sio.on('response', namespace=self.namespace)
|
||||
def on_response(data):
|
||||
print(f"Received response: {data}")
|
||||
|
||||
# ACK事件处理
|
||||
@self.sio.on('ack', namespace=self.namespace)
|
||||
def on_ack(data):
|
||||
ack_id = data.get('ack_id')
|
||||
if ack_id and ack_id in self.ack_events:
|
||||
print(f"Received ACK for message: {ack_id}")
|
||||
event = self.ack_events[ack_id]
|
||||
event.set() # 触发事件,通知等待的线程
|
||||
|
||||
def register_on_connect_entry(self, callback: Callable, entry: Any, init_data: Any)->bool:
|
||||
if not callable(callback):
|
||||
@@ -169,12 +180,15 @@ class ASEngineClient:
|
||||
self.sid = None
|
||||
print("Disconnected from server")
|
||||
|
||||
def send_text(self, route: str, text: str, id:str="") -> bool:
|
||||
def send_text(self, route: str, text: str, id:str="", wait_for_ack: bool = False, timeout: int = 5) -> bool:
|
||||
"""
|
||||
发送文本消息
|
||||
:param route: 目标路由
|
||||
:param text: 要发送的文本
|
||||
:return: 是否发送成功
|
||||
:param id: 发送者ID
|
||||
:param wait_for_ack: 是否等待服务器ACK确认
|
||||
:param timeout: 等待ACK的超时时间(秒)
|
||||
:return: 是否发送成功(若wait_for_ack为True,则返回是否收到ACK)
|
||||
"""
|
||||
if not self.connected:
|
||||
print("Not connected to server")
|
||||
@@ -183,19 +197,53 @@ class ASEngineClient:
|
||||
id = self.id
|
||||
|
||||
try:
|
||||
self.sio.emit(route, {'type': 'text', 'data': text, 'id': id}, namespace=self.namespace)
|
||||
print(f"Sent text to route '{route}': {text}")
|
||||
return True
|
||||
# 生成唯一ack_id
|
||||
ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
|
||||
|
||||
# 准备发送数据
|
||||
data = {
|
||||
'type': 'text',
|
||||
'data': text,
|
||||
'id': id,
|
||||
'ack_id': ack_id
|
||||
}
|
||||
|
||||
# 发送数据
|
||||
self.sio.emit(route, data, namespace=self.namespace)
|
||||
print(f"Sent text to route '{route}': {text[:20]}")
|
||||
|
||||
# 如果不需要等待ACK,直接返回True
|
||||
if not wait_for_ack:
|
||||
return True
|
||||
|
||||
# 初始化ACK事件
|
||||
event = Event()
|
||||
self.ack_events[ack_id] = event
|
||||
|
||||
# 等待ACK或超时
|
||||
received_ack = event.wait(timeout=timeout)
|
||||
|
||||
# 清理ACK事件
|
||||
del self.ack_events[ack_id]
|
||||
|
||||
if received_ack:
|
||||
return True
|
||||
else:
|
||||
print(f"ACK timeout for message: {ack_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to send text to route '{route}': {e}")
|
||||
return False
|
||||
|
||||
def send_voice(self, route: str, voice_data: bytes, id:str=None) -> bool:
|
||||
def send_voice(self, route: str, voice_data: bytes, id:str=None, wait_for_ack: bool = False, timeout: int = 5) -> bool:
|
||||
"""
|
||||
发送语音数据
|
||||
:param route: 目标路由
|
||||
:param voice_data: 语音数据字节流
|
||||
:return: 是否发送成功
|
||||
:param id: 发送者ID
|
||||
:param wait_for_ack: 是否等待服务器ACK确认
|
||||
:param timeout: 等待ACK的超时时间(秒)
|
||||
:return: 是否发送成功(若wait_for_ack为True,则返回是否收到ACK)
|
||||
"""
|
||||
if id is None:
|
||||
id = self.id
|
||||
@@ -204,8 +252,9 @@ class ASEngineClient:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 编码语音数据为base64
|
||||
# voice_data = 'data:image/jpeg;base64,' +base64.b64encode(voice_data).decode('utf-8')
|
||||
# 生成唯一ack_id
|
||||
ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
|
||||
|
||||
# 发送数据
|
||||
self.sio.emit(
|
||||
route,
|
||||
@@ -214,24 +263,46 @@ class ASEngineClient:
|
||||
'data': voice_data,
|
||||
'timestamp': int(time.time() * 1000),
|
||||
'sampleRate': 16000,
|
||||
'id': id
|
||||
'id': id,
|
||||
'ack_id': ack_id
|
||||
},
|
||||
namespace=self.namespace
|
||||
)
|
||||
|
||||
|
||||
print(f"Sent voice file to route '{route}'")
|
||||
return True
|
||||
|
||||
# 如果不需要等待ACK,直接返回True
|
||||
if not wait_for_ack:
|
||||
return True
|
||||
|
||||
# 初始化ACK事件
|
||||
event = Event()
|
||||
self.ack_events[ack_id] = event
|
||||
|
||||
# 等待ACK或超时
|
||||
received_ack = event.wait(timeout=timeout)
|
||||
|
||||
# 清理ACK事件
|
||||
del self.ack_events[ack_id]
|
||||
|
||||
if received_ack:
|
||||
return True
|
||||
else:
|
||||
print(f"ACK timeout for message: {ack_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to send voice to route '{route}': {e}")
|
||||
return False
|
||||
|
||||
def send_image(self, route: str, file_path: str, id:str) -> bool:
|
||||
def send_image(self, route: str, file_path: str, id:str, wait_for_ack: bool = False, timeout: int = 5) -> bool:
|
||||
"""
|
||||
发送图片文件
|
||||
:param route: 目标路由
|
||||
:param file_path: 图片文件路径
|
||||
:return: 是否发送成功
|
||||
:param id: 发送者ID
|
||||
:param wait_for_ack: 是否等待服务器ACK确认
|
||||
:param timeout: 等待ACK的超时时间(秒)
|
||||
:return: 是否发送成功(若wait_for_ack为True,则返回是否收到ACK)
|
||||
"""
|
||||
if not self.connected:
|
||||
print("Not connected to server")
|
||||
@@ -251,18 +322,41 @@ class ASEngineClient:
|
||||
# 获取文件名
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
# 生成唯一ack_id
|
||||
ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
|
||||
|
||||
# 发送数据
|
||||
self.sio.emit(
|
||||
route,
|
||||
{
|
||||
'type': 'image',
|
||||
'data': image_data,
|
||||
'id': id
|
||||
'id': id,
|
||||
'ack_id': ack_id
|
||||
},
|
||||
namespace=self.namespace
|
||||
)
|
||||
print(f"Sent image file to route '{route}': {filename}")
|
||||
return True
|
||||
|
||||
# 如果不需要等待ACK,直接返回True
|
||||
if not wait_for_ack:
|
||||
return True
|
||||
|
||||
# 初始化ACK事件
|
||||
event = Event()
|
||||
self.ack_events[ack_id] = event
|
||||
|
||||
# 等待ACK或超时
|
||||
received_ack = event.wait(timeout=timeout)
|
||||
|
||||
# 清理ACK事件
|
||||
del self.ack_events[ack_id]
|
||||
|
||||
if received_ack:
|
||||
return True
|
||||
else:
|
||||
print(f"ACK timeout for message: {ack_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to send image to route '{route}': {e}")
|
||||
return False
|
||||
@@ -286,6 +380,6 @@ class HSAEngineClient(ASEngineClient):
|
||||
super().__init__(server_url, namespace, id)
|
||||
self.chatmanager = chatmanager
|
||||
def loadmarkdown(self, fmd, fmdp, fsp):
|
||||
self.send_text('markdown-in', fmd, self.id)
|
||||
self.send_text('markdown-prompt-in', fmdp, self.id)
|
||||
self.send_text('score-prompt-in', fsp, self.id)
|
||||
self.send_text('markdown-in', fmd, self.id, wait_for_ack=True)
|
||||
self.send_text('markdown-prompt-in', fmdp, self.id, wait_for_ack=True)
|
||||
self.send_text('score-prompt-in', fsp, self.id, wait_for_ack=True)
|
||||
|
||||
@@ -57,9 +57,14 @@ def realtime_response(config: dict, realtime_action: dict) -> None:
|
||||
# emit('paste_detected', paste_data)
|
||||
|
||||
#判断一下,如果粘帖内容过多就发送,后期可加入更多条件判断
|
||||
if bb.pasted_content and len(bb.pasted_content) > 250:
|
||||
chatmanager.ase_client.send("pasted_detected_in", {"file_path": file_path, "content": bb.pasted_content})
|
||||
|
||||
if bb.pasted_content and len(bb.pasted_content) > 200:
|
||||
print(f"粘贴内容 : {bb.pasted_content[:100]}...")
|
||||
print(f"ready to send")
|
||||
try:
|
||||
result = chatmanager.ase_client.send_text("pasted_detected_in", bb.pasted_content)
|
||||
print(f"Send result: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error sending: {e}")
|
||||
|
||||
elif rtype == "fileEdit":
|
||||
file_path = realtime_action.get("filePath")
|
||||
|
||||
@@ -27,7 +27,7 @@ def get_text_file(file_name):
|
||||
response = requests.get(file_name)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(file_name)
|
||||
# print(file_name)
|
||||
# print(f"get_text_file: {response.text}")
|
||||
|
||||
return response.text
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# myapp/services/course_service.py
|
||||
import re
|
||||
from typing import Dict, List
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
from flask import current_app
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
@@ -450,10 +452,10 @@ def _default_markdowns(lesson_name: str):
|
||||
|
||||
## 阶段一:示例阶段
|
||||
### 步骤A
|
||||
请基于“步骤A”的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
|
||||
请基于"步骤A"的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
|
||||
|
||||
### 步骤B
|
||||
请基于“步骤B”的教学目标与材料,生成指导性提示词。
|
||||
请基于"步骤B"的教学目标与材料,生成指导性提示词。
|
||||
"""
|
||||
|
||||
score_prompt_md = f"""# {lesson_name}
|
||||
@@ -463,8 +465,134 @@ def _default_markdowns(lesson_name: str):
|
||||
|
||||
## 阶段一:示例阶段
|
||||
### 步骤A
|
||||
对学员在“步骤A”的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
|
||||
对学员在"步骤A"的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
|
||||
### 步骤B
|
||||
同上,对“步骤B”输出进行评分与反馈。
|
||||
同上,对"步骤B"输出进行评分与反馈。
|
||||
"""
|
||||
return lesson_md, prompt_md, score_prompt_md
|
||||
|
||||
def save_learning_progress(user_uuid: str, chatmanager, user_id: str) -> bool:
|
||||
"""
|
||||
保存用户学习进度到MongoDB
|
||||
|
||||
参数:
|
||||
- user_uuid: 用户UUID
|
||||
- chatmanager: ChatManager实例,包含学习进度数据
|
||||
- user_id: 用户ID
|
||||
|
||||
返回:
|
||||
- bool: 保存是否成功
|
||||
"""
|
||||
try:
|
||||
# 获取MongoDB连接
|
||||
mongo = current_app.extensions["mongo"]
|
||||
|
||||
# 获取MultiAgents框架中的对话列表
|
||||
dialog_list = get_multiagents_dialog_list(user_id)
|
||||
|
||||
# 构建学习进度数据
|
||||
progress_data = {
|
||||
"user_uuid": user_uuid,
|
||||
"user_id": user_id,
|
||||
"material_id": chatmanager.material_id,
|
||||
"chapter_name": chatmanager.chapter_name,
|
||||
"lesson_name": chatmanager.lesson_name,
|
||||
"chapter_chain_now": chatmanager.chapter_chain_now,
|
||||
"chat_historys": chatmanager.chat_historys,
|
||||
"scores": chatmanager.scores,
|
||||
"multiagents_dialogs": dialog_list,
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
|
||||
# 保存到MongoDB,使用upsert操作(如果存在则更新,不存在则插入)
|
||||
mongo.db.learning_progress.update_one(
|
||||
{
|
||||
"user_uuid": user_uuid,
|
||||
"material_id": chatmanager.material_id,
|
||||
"chapter_name": chatmanager.chapter_name,
|
||||
"lesson_name": chatmanager.lesson_name
|
||||
},
|
||||
{"$set": progress_data},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
current_app.logger.info(f"学习进度保存成功: user_uuid={user_uuid}, lesson={chatmanager.lesson_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"保存学习进度失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
|
||||
"""
|
||||
从MultiAgents框架获取用户对话列表
|
||||
|
||||
参数:
|
||||
- user_id: 用户ID
|
||||
|
||||
返回:
|
||||
- List[Dict]: 对话列表
|
||||
"""
|
||||
try:
|
||||
# 从配置中获取ASE引擎URL和token
|
||||
config = current_app.config
|
||||
url_token = config.get("ASE_ENGINE_URL_TOKEN", "")
|
||||
|
||||
# 构建请求URL
|
||||
base_url = config.get("ASE_ENGINE_URL", "")
|
||||
api_url = f"{base_url}/api/sync/download"
|
||||
|
||||
# 构建请求参数
|
||||
payload = {
|
||||
"namespace_url": url_token, # 使用配置中的token作为namespace_url
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
# 发送POST请求
|
||||
response = requests.post(
|
||||
api_url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取MultiAgents对话列表异常: {str(e)}")
|
||||
return []
|
||||
|
||||
def load_learning_progress(user_uuid: str, material_id: str, chapter_name: str, lesson_name: str) -> Optional[Dict]:
|
||||
"""
|
||||
加载用户学习进度
|
||||
|
||||
参数:
|
||||
- user_uuid: 用户UUID
|
||||
- material_id: 课程ID
|
||||
- chapter_name: 章节名
|
||||
- lesson_name: 课时名
|
||||
|
||||
返回:
|
||||
- Optional[Dict]: 学习进度数据,如果不存在则返回None
|
||||
"""
|
||||
try:
|
||||
mongo = current_app.extensions["mongo"]
|
||||
progress = mongo.db.learning_progress.find_one({
|
||||
"user_uuid": user_uuid,
|
||||
"material_id": material_id,
|
||||
"chapter_name": chapter_name,
|
||||
"lesson_name": lesson_name
|
||||
})
|
||||
|
||||
if progress:
|
||||
# 转换ObjectId为字符串
|
||||
if "_id" in progress:
|
||||
progress["_id"] = str(progress["_id"])
|
||||
return progress
|
||||
return None
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"加载学习进度失败: {str(e)}")
|
||||
return None
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
# myapp/sockets/namespaces.py
|
||||
import json
|
||||
import time
|
||||
from flask import current_app, request, session
|
||||
import requests
|
||||
from flask import current_app, request, session, url_for
|
||||
from flask_socketio import Namespace, join_room, leave_room, emit
|
||||
from ..config import Config
|
||||
|
||||
from ..function_tools import judge
|
||||
from ..function_tools import next_chapter
|
||||
from ..services.backboard_service import realtime_response
|
||||
from ..services.markdown_service import load_full_markdown_file
|
||||
from ..services.markdown_service import load_full_markdown_file
|
||||
from ..services.course_service import save_learning_progress
|
||||
from ..extension_ase.ase_client import HSAEngineClient
|
||||
from ..services.user_service import get_or_load_current_user
|
||||
from ..services.course_service import load_learning_progress
|
||||
|
||||
|
||||
|
||||
@@ -71,15 +75,43 @@ class AgentNamespace(Namespace):
|
||||
chatmanager = user_uuid2chatmanager[user_uuid]
|
||||
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
|
||||
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
|
||||
chatmanager.chat_historys = []
|
||||
chatmanager.scores = []
|
||||
|
||||
# 尝试加载用户学习进度
|
||||
progress = load_learning_progress(user_uuid, course_id, chapter_name, lesson_name)
|
||||
need_skip_chapters = 0
|
||||
|
||||
if progress:
|
||||
# 恢复学习进度数据
|
||||
if 'scores' in progress:
|
||||
# 根据scores的数量确定需要跳过的章节数
|
||||
need_skip_chapters = len(progress['scores'])
|
||||
chatmanager.scores = progress['scores']
|
||||
print(f"恢复用户学习进度: {user_uuid}, 已完成 {need_skip_chapters} 个章节")
|
||||
|
||||
# 恢复聊天历史
|
||||
if 'chat_historys' in progress:
|
||||
chatmanager.chat_historys = progress['chat_historys']
|
||||
else:
|
||||
chatmanager.chat_historys = []
|
||||
|
||||
# 恢复当前章节链
|
||||
if 'chapter_chain_now' in progress:
|
||||
chatmanager.chapter_chain_now = progress['chapter_chain_now']
|
||||
else:
|
||||
chatmanager.chat_historys = []
|
||||
chatmanager.scores = []
|
||||
|
||||
fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name)
|
||||
user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager)
|
||||
chatmanager.init(user_uuid, user_uuid2ase_client[user_uuid], fmd, fmdp, fsp, course_id, chapter_name, lesson_name, max_iter=5, app=current_app, socketio=self)
|
||||
backboard_manager.add_backboard(user_uuid, user_id, course_id, lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
|
||||
chatmanager.bb = backboard_manager.get_backboard(user_uuid)
|
||||
chatmanager.next_chapter()
|
||||
if not progress:
|
||||
chatmanager.next_chapter()
|
||||
|
||||
# 如果有保存的进度,需要跳过已经完成的章节
|
||||
for _ in range(need_skip_chapters):
|
||||
chatmanager.next_chapter()
|
||||
self.chatmanager = chatmanager
|
||||
self.ase_client = user_uuid2ase_client[user_uuid]
|
||||
user_uuid2ase_client[user_uuid].register_on_connect_entry(
|
||||
@@ -180,9 +212,28 @@ class AgentNamespace(Namespace):
|
||||
print(f"on_connect_to_ase {client_entry}, {namespace_entry}, {init_data}")
|
||||
namespace_entry, ase_client = namespace_entry
|
||||
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
|
||||
namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent')
|
||||
|
||||
try:
|
||||
base_url = current_app.config['ASE_ENGINE_URL']
|
||||
clear_url = f"{base_url}/clear_user_session"
|
||||
payload = {
|
||||
'namespace_url': current_app.config['ASE_ENGINE_URL_TOKEN'], # WebSocket命名空间URL
|
||||
'user_id': init_data['room'], # 用户ID,对应session_info.id
|
||||
'clear_type': 'all' # 清理类型,默认为全部
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
response = requests.post(clear_url, json=payload, headers=headers)
|
||||
response.raise_for_status() # 检查请求是否成功
|
||||
print(f"Clear user session successful: {response.json()}")
|
||||
except Exception as e:
|
||||
print(f"Error clearing user session: {e}")
|
||||
ase_client.chatmanager.load_next_chapter()
|
||||
ase_client.send_text("chapter-start", "")
|
||||
namespace_entry.emit('message', "Multi-Agents服务已连接", room=init_data['room'], namespace='/agent')
|
||||
namespace_entry.emit('message', "教案加载完毕", room=init_data['room'], namespace='/agent')
|
||||
|
||||
|
||||
#接受消息回来直接让message监听发送
|
||||
def receive_ase_paste_detected(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
|
||||
@@ -220,9 +271,8 @@ class AgentNamespace(Namespace):
|
||||
if len(chatmanager.scores) >= chatmanager.chapter_chain_now:
|
||||
chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None)
|
||||
else: chatmanager.scores.append(data.get('data', None))
|
||||
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.material_id, chatmanager.chapter_name, chatmanager.lesson_name)
|
||||
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.course_id, chatmanager.chapter_name, chatmanager.lesson_name)
|
||||
chatmanager.load_next_chapter()
|
||||
time.sleep(0.5)
|
||||
chatmanager.ase_client.send_text("chapter-start", "")
|
||||
|
||||
|
||||
@@ -240,10 +290,32 @@ class AgentNamespace(Namespace):
|
||||
|
||||
|
||||
def on_disconnect(self,data):
|
||||
print(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}')
|
||||
ex = current_app.extensions
|
||||
uuid = session.get('user_uuid')
|
||||
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
|
||||
user_uuid2chatmanager[uuid].disconnect(uuid)
|
||||
user_uuid2chatmanager = ex.get("user_uuid2chatmanager", {})
|
||||
|
||||
# 保存学习进度到MongoDB
|
||||
try:
|
||||
# 获取用户ID
|
||||
uuid2username = ex.get("uuid2username", {})
|
||||
user_id = uuid2username.get(uuid, "unknown")
|
||||
|
||||
# 调用保存学习进度功能
|
||||
chatmanager = user_uuid2chatmanager.get(uuid)
|
||||
if chatmanager:
|
||||
save_result = save_learning_progress(uuid, chatmanager, user_id)
|
||||
print(f"保存学习进度结果: {save_result}, user_uuid={uuid}")
|
||||
except Exception as e:
|
||||
print(f"保存学习进度时发生异常: {str(e)}")
|
||||
|
||||
# 执行原有的断开连接逻辑
|
||||
if uuid in user_uuid2chatmanager:
|
||||
user_uuid2chatmanager[uuid].disconnect(uuid)
|
||||
|
||||
# 离开房间
|
||||
if uuid:
|
||||
leave_room(uuid, namespace='/agent')
|
||||
|
||||
print("VSCode client disconnected")
|
||||
print("Disconnect reason:"+str(data))
|
||||
32
Html/apps/static/68bacdfadf5aeae0912f7f18-第七周-新7周课时.html
Normal file
32
Html/apps/static/68bacdfadf5aeae0912f7f18-第七周-新7周课时.html
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>新7周课时</h1>
|
||||
<h2>阶段一:示例阶段</h2>
|
||||
<h3>步骤A</h3>
|
||||
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
|
||||
<h3>步骤B</h3>
|
||||
<p>继续补充该阶段的其它步骤内容。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-比较型排序.html
Normal file
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-比较型排序.html
Normal file
@@ -0,0 +1,159 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>基于比较的排序</h1>
|
||||
<h2>基于比较的排序</h2>
|
||||
<h3>优先队列的原理</h3>
|
||||
<p><strong>优先队列(Priority Queue)</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
|
||||
<br>
|
||||
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
|
||||
<br><br>
|
||||
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
|
||||
<br><br>
|
||||
<strong>堆(Heap)结构</strong>是实现优先队列的首选。</p>
|
||||
<h3>数组模拟堆实现的优先队列</h3>
|
||||
<h4>堆结构与取存原理</h4>
|
||||
<p>堆(Heap)本质上是一个完全二叉树结构:
|
||||
(当然也可以是多叉树,但没有必要)
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1:大根堆的二叉树结构图示" /></p>
|
||||
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
|
||||
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
|
||||
<h5>取数</h5>
|
||||
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
|
||||
但是将这个数取出后还不能结束,因为需要维护堆的性质。
|
||||
<br>
|
||||
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
|
||||
<br></p>
|
||||
<h5>存数</h5>
|
||||
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
|
||||
<h4>数组模拟堆</h4>
|
||||
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2:左边是堆的数组表示,右边是其对应的二叉树" />
|
||||
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
|
||||
<p>数组下标从1开始,任意一个下标i,其左右儿子下标刚好就是"i*2"和“i*2+1”。这样在后续代码实现时,代码写起来就会简单很多。</p>
|
||||
<h6>建堆</h6>
|
||||
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
|
||||
对于一个元素随机的数组,只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
|
||||
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
|
||||
<h3>优先队列的算法实现</h3>
|
||||
<h4>练习:堆操作</h4>
|
||||
<p>在进行代码实现之前,做一个问题练习:
|
||||
对于一个随机队列"[3, 32, 6, 43, 5, 8, 0, 9]",经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
|
||||
将答案告知AI教师。</p>
|
||||
<h4>任务:城市事件优先调度</h4>
|
||||
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
|
||||
<p><br><br></p>
|
||||
<h4>题目:实现堆排序</h4>
|
||||
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
|
||||
<br>
|
||||
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
|
||||
<br>
|
||||
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
|
||||
<br></p>
|
||||
<h5>代码框架</h5>
|
||||
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def max_heapify(arr, n, i):
|
||||
"""
|
||||
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
|
||||
调整结点 i 使以其为根的子树成为最大堆
|
||||
参数:
|
||||
arr: 存储堆的数组
|
||||
n: 堆的有效大小(长度)
|
||||
i: 需要下滤调整的节点索引
|
||||
"""
|
||||
# TODO: 在此处实现 "下滤" 操作,将 arr[i] 下沉到正确位置
|
||||
|
||||
|
||||
def build_max_heap(arr):
|
||||
"""
|
||||
将无序数组原地建成最大堆,从后往前进行下滤
|
||||
参数:
|
||||
arr: 待调整的数组
|
||||
"""
|
||||
# TODO: 调用 max_heapify 将 arr 调整为堆
|
||||
|
||||
|
||||
def heap_sort(arr):
|
||||
"""
|
||||
利用堆排序算法排序数组(降序)
|
||||
参数:
|
||||
arr: 待排序数组
|
||||
返回:
|
||||
排序后的数组(从大到小)
|
||||
"""
|
||||
# TODO: 完成堆排序的实现
|
||||
n = len(arr)
|
||||
# 1. 原地建堆
|
||||
build_max_heap(arr)
|
||||
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
|
||||
|
||||
|
||||
#性能测试:对比不同规模输入的堆排序用时
|
||||
def measure(sort_func, data):
|
||||
start = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end = time.perf_counter_ns()
|
||||
return (end - start) / 10**6 # 毫秒
|
||||
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("堆排序性能测试:")
|
||||
for n in sizes:
|
||||
data = [random.randint(0, 1000000) for _ in range(n)]
|
||||
t = measure(heap_sort, data)
|
||||
print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>实验结果分析</h5>
|
||||
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>,当输入规模增大时,运行时间应呈现近似线性乘以对数的增长趋势。具体来说,若将输入规模扩大10倍,运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
|
||||
<br>
|
||||
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比,你会发现堆排序对规模扩大的响应增长显著缓慢得多。
|
||||
<br>
|
||||
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
|
||||
<br>
|
||||
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
|
||||
<h3>比较排序的决策树模型</h3>
|
||||
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
|
||||
<br>
|
||||
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>。
|
||||
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
|
||||
<p>决策树是描述比较排序过程的一种抽象模型。
|
||||
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
|
||||
<br>
|
||||
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
|
||||
<br>
|
||||
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
|
||||
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
|
||||
<br>
|
||||
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
|
||||
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
|
||||
<br>
|
||||
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
|
||||
<br>
|
||||
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
|
||||
<br>
|
||||
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>,因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符:对三个无任何特殊性质的数进行排序,最少需要3次比较才能确定它们的正确顺序。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
219
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-非比较型排序.html
Normal file
219
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-非比较型排序.html
Normal file
@@ -0,0 +1,219 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>非比较型排序</h1>
|
||||
<h2>非比较型排序</h2>
|
||||
<h3>基数排序原理</h3>
|
||||
<h4>排序算法复杂度背景</h4>
|
||||
<p>基于比较的排序算法,理论上无法突破(Ω(n \log n))时间复杂度下界
|
||||
特殊场景下,不依赖元素间比较、利用元素结构特征,可实现线性时间排序</p>
|
||||
<h4>基数排序核心思想</h4>
|
||||
<p>将元素视为由多个 “位”(digit)组成
|
||||
按位进行多轮排序,每轮针对一个关键位
|
||||
依赖稳定的子排序算法,保证先前排好的顺序不被打乱
|
||||
关键保障:<strong>排序的稳定性(相同元素相对顺序不变)</strong></p>
|
||||
<h4>经典示例(十进制整数的基数排序):</h4>
|
||||
<p>按最低有效位(LSB)→ 最高有效位(MSB) 逐轮排序
|
||||
第一轮:按个位数排序(0-9 分组)
|
||||
后续轮次:依次按十位数、百位数等排序</p>
|
||||
<h4>实操流程:</h4>
|
||||
<p>初始序列:[329, 457, 657, 839, 436]
|
||||
第一轮(按个位排序):
|
||||
按个位 0-9 分组 → [436(6), 457(7), 657(7), 329(9), 839(9)]
|
||||
(注:457 与 657 个位相同,保持原顺序)
|
||||
第二轮(按十位排序):
|
||||
按十位 0-9 分组 → [329(2), 436(3), 839(3), 457(5), 657(5)]
|
||||
(注:436 与 839 十位相同,保持第一轮后的顺序)
|
||||
第三轮(按百位排序):
|
||||
按百位 0-9 分组 → [329(3), 436(4), 457(4), 657(6), 839(8)]
|
||||
(注:436 与 457 百位相同,保持第二轮后的顺序)</p>
|
||||
<p>每轮排序通过稳定性保留前序结果,最终实现 “高位决定整体顺序”,完成正确排序。</p>
|
||||
<h4>时间复杂度:</h4>
|
||||
<p>每轮扫一遍序列O(n),总共轮数就是基数位数,设为k。
|
||||
时间复杂度为O(kn),其中k一般不太大。</p>
|
||||
<h3>基数排序的实现</h3>
|
||||
<p>为了加深对基数排序的理解,我们通过一个编程任务来实践其实现。我们将以整数排序为例,并采用从最低有效位开始的方法对数字进行排序(LSD 基数排序)。</p>
|
||||
<p>在实现过程中,我们需要编写一个按某个位数进行计数排序的辅助函数,然后在主函数中对每一位循环调用该辅助函数。</p>
|
||||
<h4>用计数排序稳定地对基数排序</h4>
|
||||
<p>每选定一个基数后,需要用O(n)的时间复杂度将当前序列按照这个基数进行排序;或说<strong>找到按照当前基数下,当前序列的每一个数要前往的位置</strong>
|
||||
这里用“计数排序”这个算法。
|
||||
具体做法如下:</p>
|
||||
<ol>
|
||||
<li>建一个新数组count,记录当前基数的每个不同的数有多少个(计数)</li>
|
||||
<li>从前往后遍历序列,取出序列每一个元素在当前基数位置上的值,对应计数值+1(count[number]+=1)</li>
|
||||
<li>将count转变为前缀和,这样就得到了每一个基数对应的原始数的最后一个,在新序列中的位置</li>
|
||||
<li>从后往前遍历原序列,将每一个数(基数上的数为number)放到对应新序列count[number]-1的位置,并让count[number]-=1。</li>
|
||||
</ol>
|
||||
<h5>练习:按位计数排序</h5>
|
||||
<p>作为练习,[170, 45, 75, 90, 802, 24, 2, 66],按个位排序(exp=1)进行计数排序,请写下count数组的计数结果(2.步后)和前缀和结果。并表述在第4步的利用方式,从而得到序列结果。</p>
|
||||
<h4>任务:实现 LSD 基数排序</h4>
|
||||
<p>请实现 radix_sort(arr) 函数,对传入的整数列表进行从小到大的排序。你可以假设所有整数为非负且位数相对固定。为了简化问题,我们以十进制为基数(基数=10)实现,并假定输入整数的最大位数为<eq>d</eq>(例如<eq>10^d</eq>数量级)。算法思路如下:</p>
|
||||
<p>从最低位(个位,exp=1)开始,对数组进行计数排序(Counting Sort),按该位的数值对元素排序。</p>
|
||||
<p>然后依次向更高一位(exp=10,exp=100,...)进行排序,直到处理完最高位。</p>
|
||||
<h4>代码框架</h4>
|
||||
<p>请在下方代码编辑区完成 counting_sort_by_digit 和 radix_sort 函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def counting_sort_by_digit(arr, exp):
|
||||
"""
|
||||
对数组按某个数位进行计数排序(稳定的)。
|
||||
参数:
|
||||
arr: 要排序的数组
|
||||
exp: 位标志,表示当前要按照 (exp位) 上的数值进行排序,
|
||||
例如 exp=1 表示按个位排序,exp=10 表示按十位排序。
|
||||
返回:
|
||||
按指定位排序后的数组副本
|
||||
"""
|
||||
n = len(arr)
|
||||
output = [0] * n # 存放排序结果
|
||||
count = [0] * 10 # 计数数组(十进制位范围0-9)
|
||||
# TODO: 计算每个数组元素在该位上的值,并计数
|
||||
|
||||
|
||||
def radix_sort(arr):
|
||||
"""
|
||||
基数排序(按从低位到高位逐位排序)
|
||||
参数:
|
||||
arr: 待排序的整数列表
|
||||
返回:
|
||||
按非递减顺序排序的列表
|
||||
"""
|
||||
#TODO: 利用 counting_sort_by_digit 按位排序数组
|
||||
|
||||
|
||||
#测试与性能分析
|
||||
def measure(sort_func, data):
|
||||
start = time.perf_counter_ns()
|
||||
sort_func(data)
|
||||
end = time.perf_counter_ns()
|
||||
return (end - start) / 10**6 # 转换为毫秒
|
||||
|
||||
#生成随机测试数据
|
||||
sizes = [10000, 50000, 100000]
|
||||
print("基数排序性能测试:")
|
||||
for n in sizes:
|
||||
data = [random.randrange(0, 1000000) for _ in range(n)]
|
||||
t = measure(radix_sort, data)
|
||||
print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")
|
||||
</code></pre>
|
||||
<p>性能讨论</p>
|
||||
<p>在完成代码并运行测试后,可以看到基数排序在不同规模输入下运行时间增长相对温和。理想情况下,对于固定位数<eq>d</eq>的整数,基数排序的时间复杂度为<eq>O(d \times (n + k))</eq>,其中<eq>k</eq>为每位可能的取值数量(对十进制整数而言<eq>k=10</eq>)。
|
||||
<br>
|
||||
当<eq>d</eq>视作常数时,复杂度近似<eq>O(n)</eq>,因此输入规模扩大倍数时,运行时间应近似线性增长。
|
||||
<br>
|
||||
然而需注意,基数排序的实际常数因子不小(多次稳定排序和额外空间),对中等规模数据Python实现未必比C语言内建排序更快。
|
||||
<br>
|
||||
更重要的是,基数排序并非万能:如果元素位数<eq>d</eq>随<eq>n</eq>增长(例如排序<eq>1</eq>到<eq>N</eq>范围的数,<eq>N</eq>约为<eq>n</eq>量级),则<eq>d = O(\log n)</eq>,此时基数排序复杂度<eq>≈O(n \log n)</eq>,并没有打破比较排序下界。因此,基数排序的线性性能依赖于位数相对较小这一前提。</p>
|
||||
<h3>桶排序的原理与实现</h3>
|
||||
<h4>桶排序核心定位</h4>
|
||||
<p>典型的线性时间排序算法
|
||||
利用输入数据的<strong>分布特性(假设大致均匀分布在某范围)</strong></p>
|
||||
<h4>桶排序原理与类比</h4>
|
||||
<h5>核心逻辑(三步):</h5>
|
||||
<p>第一步:创建若干 “桶”,将元素按值映射到对应桶中(实现粗分类)
|
||||
第二步:对每个桶内元素单独排序(可选用任意排序算法)
|
||||
第三步:按桶的顺序合并所有元素,得到有序序列
|
||||
<br>
|
||||
映射:将小数按值投入对应桶,每个桶仅覆盖原始区间 1/10,数据量少
|
||||
桶内排序:因元素少,插入排序等简单算法足够快
|
||||
合并:按桶序号收集,因 “i<j 时,第 i 号桶元素<第 j 号桶元素”,天然有序
|
||||
<br></p>
|
||||
<h5>案例练习</h5>
|
||||
<p>使用桶排序算法对以下 8 个数字进行排序
|
||||
[35, 12, 48, 27, 5, 39, 18, 42]
|
||||
以48为分母,将其他所有数划分到0-0.2-0.4-0.6-0.8-1这5个桶中,输出每个桶里面的数字是什么。
|
||||
<br><br></p>
|
||||
<h4>代码实现</h4>
|
||||
<p>请实现 bucket_sort(arr) 函数,将传入的[0,1)区间的小数数组排序。思路如下:
|
||||
<br>
|
||||
创建若干个空桶(列表),桶的数量可以根据数据规模n选择,这里取<eq>n/10</eq>(为整数)个桶。
|
||||
<br>
|
||||
将每个元素按照其值乘以桶数量后的整数部分,映射到对应的桶中。例如值为0.23、桶数为10时,<eq>0.23 \times 10 = 2.3</eq>,放入索引2号桶。
|
||||
<br>
|
||||
对每个非空桶内部进行排序。你可以直接使用 Python 内置排序(sorted)或实现简单排序算法。</p>
|
||||
<p>按桶的顺序依次合并桶内元素,得到排序后的结果。</p>
|
||||
<h5>代码框架</h5>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def bucket_sort(arr):
|
||||
"""
|
||||
桶排序(适用于0到1区间的小数)
|
||||
参数:
|
||||
arr: 介于[0,1)的浮点数列表
|
||||
返回:
|
||||
升序排序后的列表
|
||||
"""
|
||||
# TODO: 按原理实现桶排序
|
||||
n = len(arr)
|
||||
if n == 0:
|
||||
return arr
|
||||
# 1. 创建桶
|
||||
|
||||
# 2. 分配元素到各桶
|
||||
|
||||
# 3. 桶内排序
|
||||
|
||||
|
||||
#简单测试与性能评估
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("桶排序性能测试:")
|
||||
for n in sizes:
|
||||
#生成n个[0,1)均匀分布的小数
|
||||
data = [random.random() for _ in range(n)]
|
||||
start = time.perf_counter_ns()
|
||||
sorted_data = bucket_sort(data)
|
||||
end = time.perf_counter_ns()
|
||||
elapsed = (end - start) / 10**6
|
||||
#验证排序正确性
|
||||
is_correct = "√" if sorted_data == sorted(data) else "×"
|
||||
print(f"数据规模 n={n}: 排序耗时 {elapsed:.2f} ms, 排序正确性 {is_correct}")
|
||||
|
||||
</code></pre>
|
||||
<h4>桶排序时间复杂度分析</h4>
|
||||
<h5>平均情况(理想分布):</h5>
|
||||
<p>前提:n 个元素、m 个桶,数据均匀分布 → 每个桶平均含 n/m 个元素
|
||||
优化:选 m 接近 n 的量级 → 桶内排序成本视为常数
|
||||
结果:整体期望运行时间为O(n)(线性时间)</p>
|
||||
<h5>最坏情况(分布不均):</h5>
|
||||
<p>极端场景:所有元素落入同一个桶
|
||||
复杂度:取决于桶内排序算法(O (n log n) 或 O (n²))+ 分配桶的 O (n)
|
||||
结果:退化至O (n log n) 甚至更差,失去线性时间优势
|
||||
结论:理想分布下性能出众,数据分布不均时无优势</p>
|
||||
<h3>桶排序与哈希</h3>
|
||||
<h4>核心相通点:映射分散思想</h4>
|
||||
<p>通过函数映射将元素分散到不同 “容器”(桶排序的 “桶”/ 哈希算法的 “哈希桶”),把原问题拆解为小的子问题
|
||||
<br>
|
||||
桶排序:用映射函数(如 index = int(num * bucket_count))划分元素所属桶,依据元素值分配
|
||||
哈希算法:用哈希函数计算键的哈希桶索引,确定元素存储位置
|
||||
这种映射也意味着都对<strong>分布均匀性的依赖</strong></p>
|
||||
<h4>关键区别:映射函数的目标差异</h4>
|
||||
<p>对比维度
|
||||
桶排序的映射函数:一般直接做除法:保留元素大小顺序(按值范围划分)
|
||||
哈希算法的哈希函数:随机映射函数,实现元素均匀分布,不能用于排序(除非特殊设计有序哈希)</p>
|
||||
<h4>总结</h4>
|
||||
<p>桶排序可看作哈希思想在排序问题中的应用:均通过 “先分类、后处理” 的范式优化性能,但因目标不同,映射函数设计存在本质差异。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第二章:分治法-分治法实践与优化.html
Normal file
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第二章:分治法-分治法实践与优化.html
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>分治策略进阶与主方法</h1>
|
||||
<blockquote>
|
||||
<p>Auto-generated at 2025-09-22 15:44</p>
|
||||
</blockquote>
|
||||
<h2>分治策略进阶与主方法</h2>
|
||||
<h3>最大子数组问题</h3>
|
||||
<h4>任务:编码与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
|
||||
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>,总增量最大的连续时段是从第8天到第11天,总增量为 <code>18 + 20 - 7 + 12 = 43</code> 。
|
||||
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
|
||||
<h5>题目:最大交通流量增量</h5>
|
||||
<p>你需要实现一个分治算法来解决最大子数组问题。
|
||||
同时,为了对比,你也会实现一个暴力求解算法。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
|
||||
<pre><code class="language-python">import time
|
||||
import random
|
||||
|
||||
def find_maximum_subarray_brute(arr):
|
||||
"""
|
||||
使用暴力法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
|
||||
#--- 本周任务:请在下方实现分治算法 ---
|
||||
|
||||
def find_max_crossing_subarray(arr, low, mid, high):
|
||||
"""
|
||||
找到跨越中点的最大子数组
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现寻找跨越中点的最大子数组的逻辑
|
||||
# 提示: 从mid向左和向右分别扫描,找到各自的最大和
|
||||
|
||||
def find_maximum_subarray(arr, low, high):
|
||||
"""
|
||||
使用分治法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现分治递归逻辑
|
||||
# 提示: 递归基是当数组只有一个元素时
|
||||
|
||||
#--- 测试与对比部分 --- 以下内容无需修改
|
||||
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
|
||||
print(f"交通流量变化数据: {traffic_changes}")
|
||||
|
||||
#使用分治法
|
||||
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
|
||||
print(f"分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}")
|
||||
|
||||
#使用暴力法验证
|
||||
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
|
||||
print(f"暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}")
|
||||
|
||||
#性能测试
|
||||
large_data = [random.randint(-50, 50) for _ in range(2000)]
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray(large_data, 0, len(large_data) - 1)
|
||||
dc_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms")
|
||||
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray_brute(large_data)
|
||||
brute_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms")
|
||||
</code></pre>
|
||||
<h3>分治经典算法:快速排序</h3>
|
||||
<h4>快速排序原理</h4>
|
||||
<p>快速排序(Quick Sort)是分治法的经典应用之一。与归并排序不同的是,快速排序通过原地划分数组来完成排序,无需额外的同规模辅助存储空间。它在平均情况下表现极为高效,是实际应用中常用的排序算法之一。</p>
|
||||
<p>快速排序的分治核心思路非常好理解,对于一个未排序的数组,希望分解为左区间和一个“枢轴(pivot)”元素和右区间,其中左区间的数都小于等于枢轴,右区间的都大于等于枢轴。
|
||||
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
|
||||
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
|
||||
<h4>任务:编码与实验</h4>
|
||||
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
|
||||
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
|
||||
代码框架
|
||||
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
实现快速排序算法(固定枢轴策略)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
# TODO: 实现快速排序的分治逻辑
|
||||
# 提示:选取第一个元素为枢轴,递归排序左右子数组
|
||||
|
||||
def measure_performance(sort_func, data):
|
||||
start_time = time.perf_counter_ns()
|
||||
sort_func(data.copy()) # 对数据副本排序,避免影响原数据
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6 # 转换为毫秒
|
||||
|
||||
#性能测试:对比有序输入和随机输入
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("快速排序性能测试(固定枢轴):")
|
||||
for n in sizes:
|
||||
sorted_data = list(range(n))
|
||||
random_data = [random.randint(0, 10**6) for _ in range(n)]
|
||||
time_sorted = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(quick_sort, random_data)
|
||||
print(f"数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms")
|
||||
</code></pre>
|
||||
<p>完成编码并运行测试,报告时间差异。</p>
|
||||
<h3>快速排序的复杂度分析</h3>
|
||||
<h4>时间复杂度</h4>
|
||||
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong>和</strong>递归深度</strong>共同决定
|
||||
三种典型场景:最坏情况、平均情况、最佳情况</p>
|
||||
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
|
||||
平均情况:输入随机 / 等概率选枢轴
|
||||
最佳情况:每次划分平分数组</p>
|
||||
<h4>空间复杂度</h4>
|
||||
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
|
||||
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
|
||||
<h4>分析</h4>
|
||||
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
|
||||
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
|
||||
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
|
||||
<h4>实践</h4>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def random_quick_sort(arr):
|
||||
"""
|
||||
实现随机快速排序算法(随机选择枢轴)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
你在上一章实现的快排内容
|
||||
"""
|
||||
|
||||
#验证随机快排在极端有序输入下的性能
|
||||
n = 10000
|
||||
sorted_data = list(range(n))
|
||||
time_fixed = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(random_quick_sort, sorted_data)
|
||||
print(f"数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms")
|
||||
|
||||
</code></pre>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>动态规划例题训练</h1>
|
||||
<h2>动态规划例题训练</h2>
|
||||
<h3>步骤A</h3>
|
||||
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
|
||||
<h3>步骤B</h3>
|
||||
<p>继续补充该阶段的其它步骤内容。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
240
Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
Normal file
240
Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>动态规划原理</h1>
|
||||
<h2>动态规划原理</h2>
|
||||
<h3>自顶而下的分治 vs. 自底向上的动态规划</h3>
|
||||
<p>分治法:面对规模为 <em>n</em> 的问题,从<strong>顶层</strong>出发,将其拆为若干个更小的子问题,分别求解后<strong>合并</strong>。</p>
|
||||
<p>动态规划(DP):当<strong>同一子问题重复出现</strong>时,与其一再从顶层“把问题拆碎”,不如记录子问题的答案,并按规模<strong>从小到大</strong>把所有需要的子问题一次性求出来。
|
||||
<br></p>
|
||||
<h4>什么才算“同一”子问题</h4>
|
||||
<p>与AI教师讨论,如何区分“相同规模的子问题”和“同一(可复用的)子问题”</p>
|
||||
<h3>切绳(Rod Cutting)问题</h3>
|
||||
<p>给定一根长度为 <em>n</em> 的绳子,价格表 <code>price[i]</code> 表示长度为 <em>i</em> 的一段可以卖出的价格。允许将绳子切成多段出售,目标是使总收益最大。</p>
|
||||
<h4>分治法与时间复杂度</h4>
|
||||
<p>设 <code>R(n)</code> 为长度 <em>n</em> 的最大收益:</p>
|
||||
<section>
|
||||
<eqn> R(0) = 0, R(1)=price[1] </eqn>
|
||||
</section>
|
||||
<section>
|
||||
<eqn> R(n) = max_{1 ≤ i ≤ n} ( price[i] + R(n - i) ) </eqn>
|
||||
</section>
|
||||
<p><strong>纯递归分治</strong>会对同一规模的子问题多次求解,子问题规模组合数呈指数级,时间复杂度为 <strong>O(n^n)</strong> 量级。</p>
|
||||
<h4>记忆化(自顶向下)</h4>
|
||||
<p>思想:用哈希表/数组 <code>memo[n]</code> 记录 <code>R(n)</code>。当再次需要 <code>R(n)</code> 时,直接返回已存结果,避免重复计算。</p>
|
||||
<ul>
|
||||
<li><strong>复杂度</strong>:时间 <strong>O(n^2)</strong>(外层 n,内层枚举切第一刀 i),空间 <strong>O(n)</strong>。</li>
|
||||
</ul>
|
||||
<p><strong>代码任务 A:实现记忆化递归(Top-Down)</strong></p>
|
||||
<pre><code class="language-python">def rod_cut_topdown(price: dict[int, int], n: int, memo: list[int]) -> int:
|
||||
"""
|
||||
返回长度 n 的最大收益(记忆化递归)。
|
||||
TODO:
|
||||
1) 处理 n==0;2) 命中 memo 直接返回;3) 枚举第一刀长度 i;4) 写回 memo[n]。
|
||||
"""
|
||||
pass
|
||||
</code></pre>
|
||||
<h4>自底向上(反向记忆化)</h4>
|
||||
<p>将规模从小到大推进:<code>dp[x]</code> 表示长度 <code>x</code> 的最优收益。
|
||||
<strong>代码任务 B:实现自底向上(Bottom-Up)</strong></p>
|
||||
<pre><code class="language-python">def rod_cut_bottomup(price: dict[int, int], n: int) -> int:
|
||||
"""
|
||||
返回长度 n 的最大收益(自底向上)。
|
||||
TODO:
|
||||
1) 初始化 dp[0]=0;2) for x in 1..n:dp[x] = max_{1..x}( price[i] + dp[x-i] )
|
||||
"""
|
||||
|
||||
"""
|
||||
以下内容无需修改,注意将你实现的rod_cut_topdown代码复制过来
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import signal
|
||||
from functools import wraps
|
||||
|
||||
|
||||
#超时异常定义
|
||||
class TimeoutError(Exception):
|
||||
pass
|
||||
|
||||
#超时装饰器
|
||||
def timeout(seconds):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 定义超时处理函数
|
||||
def handle_timeout(signum, frame):
|
||||
raise TimeoutError(f"Function {func.__name__} timed out after {seconds} seconds")
|
||||
|
||||
# 设置信号处理
|
||||
signal.signal(signal.SIGALRM, handle_timeout)
|
||||
signal.alarm(seconds) # 触发超时
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
finally:
|
||||
signal.alarm(0) # 取消超时
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
#带超时的纯递归实现(用于对比)
|
||||
@timeout(1) # 1秒超时
|
||||
def rod_cut_recursive(price: dict[int, int], n: int) -> int:
|
||||
if n == 0:
|
||||
return 0
|
||||
max_rev = -float('inf')
|
||||
for i in range(1, n + 1):
|
||||
if i in price:
|
||||
max_rev = max(max_rev, price[i] + rod_cut_recursive(price, n - i))
|
||||
return max_rev
|
||||
|
||||
|
||||
#对比实验
|
||||
def compare_algorithms():
|
||||
# 生成测试用的价格表(随机生成1到10的价格)
|
||||
max_length = 50
|
||||
price = {i: random.randint(1, 10) for i in range(1, max_length + 1)}
|
||||
|
||||
print(f"{'n':<5} {'递归(ms)':<10} {'记忆化(ms)':<12} {'自底向上(ms)':<15}")
|
||||
print("-" * 50)
|
||||
|
||||
# 测试n从10到50的情况
|
||||
for n in range(10, 51, 5):
|
||||
# 纯递归(带超时处理)
|
||||
try:
|
||||
start = time.time()
|
||||
recursive_result = rod_cut_recursive(price, n)
|
||||
recursive_time = (time.time() - start) * 1000
|
||||
except TimeoutError:
|
||||
recursive_result = "超时"
|
||||
recursive_time = ">1000"
|
||||
|
||||
# 记忆化递归
|
||||
memo = [-1] * (n + 1)
|
||||
start = time.time()
|
||||
topdown_result = rod_cut_topdown(price, n, memo)
|
||||
topdown_time = (time.time() - start) * 1000
|
||||
|
||||
# 自底向上
|
||||
start = time.time()
|
||||
bottomup_result = rod_cut_bottomup(price, n)
|
||||
bottomup_time = (time.time() - start) * 1000
|
||||
|
||||
# 验证结果一致性(仅当递归未超时)
|
||||
if isinstance(recursive_result, int):
|
||||
assert recursive_result == topdown_result == bottomup_result, f"结果不一致 for n={n}"
|
||||
|
||||
# 输出结果
|
||||
print(f"{n:<5} {recursive_time:<10} {topdown_time:<12.4f} {bottomup_time:<15.4f}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
compare_algorithms()
|
||||
</code></pre>
|
||||
<p>完成上面的代码,讨论实验对比结果。</p>
|
||||
<h3>动态规划的“组成”:如何写出一个 DP</h3>
|
||||
<h4><strong>四大组成</strong></h4>
|
||||
<ul>
|
||||
<li><strong>状态空间</strong>:用最少的下标(或维度)刻画子问题(如 <code>dp[i]</code>、<code>dp[i][j]</code>)。</li>
|
||||
<li><strong>状态转移</strong>:写出“从更小状态到当前状态”的递推/转移式。</li>
|
||||
<li><strong>边界条件</strong>:初始已知的最小规模答案(如 <code>dp[0]=0</code>)。</li>
|
||||
<li><strong>解的恢复(部分题目可能不需要)</strong>:若需输出方案/路径,记录子问题选择来源(从那个子问题的答案转移而来)(如 <code>choice[i][j]</code>)。</li>
|
||||
</ul>
|
||||
<h4><strong>两大性质</strong>:</h4>
|
||||
<ul>
|
||||
<li><strong>最优子结构</strong>:全局最优由若干子问题的最优解组合而成;</li>
|
||||
<li><strong>重复子问题</strong>:不同路径会遇到同一个(或等价的)子问题。</li>
|
||||
</ul>
|
||||
<h3>例题:矩阵连乘(Matrix-Chain Multiplication, MCM)</h3>
|
||||
<h4>问题描述</h4>
|
||||
<p>矩阵乘法有严格的维度匹配要求:只有前一个矩阵的列数 = 后一个矩阵的行数,才能相乘。
|
||||
即:
|
||||
矩阵 A:维度为 m × k(共 m 行、k 列)
|
||||
矩阵 B:维度为 k × n(共 k 行、n 列)
|
||||
矩阵 C = A×B,维度为 m × n
|
||||
产生标量乘法次数为 <code>m×n×k</code>
|
||||
给定矩阵链 <code>A₁A₂…Aₖ</code>,维度数组 <code>p[0..k]</code> 满足 <code>Aᵢ</code> 大小为 <code>p[i-1] × p[i]</code>。目标:只改变乘法<strong>括号化顺序</strong>,最小化标量乘法次数。</p>
|
||||
<h4>递推与 DP 表</h4>
|
||||
<p>令 <code>m[i][j]</code> 表示从 <code>Aᵢ…Aⱼ</code> 的最小乘法次数(1-index)。则:</p>
|
||||
<section>
|
||||
<eqn> m[i][i] = 0 </eqn>
|
||||
</section>
|
||||
<section>
|
||||
<eqn> m[i][j] = min_{i ≤ k < j} ( 尝试推导一下这里的转移式 ) </eqn>
|
||||
</section>
|
||||
<h4>记录断点</h4>
|
||||
<p>令 <code>s[i][j]</code> 存储从 <code>Aᵢ…Aⱼ</code> 的最优断点。
|
||||
<eq>$ s[i][j] = k \text{ if } m[i][j] == 与上面的式子一样 $</eq></p>
|
||||
<h4>代码任务</h4>
|
||||
<pre><code class="language-python">def matrix_chain_order(p: list[int]) -> tuple[list[list[int]], list[list[int]]]:
|
||||
"""
|
||||
返回 (m, s),m[i][j] 为最小代价,s[i][j] 为最优断点。
|
||||
TODO: 长度 n = len(p)-1;按区间长度 L=2..n 填表。
|
||||
"""
|
||||
...
|
||||
|
||||
def print_optimal_parens(s: list[list[int]], i: int, j: int) -> str:
|
||||
"""根据断点矩阵 s 输出最优括号化方案"""
|
||||
if i == j:
|
||||
return f"A{i}"
|
||||
else:
|
||||
return f"({print_optimal_parens(s, i, s[i][j])}" \
|
||||
f"{print_optimal_parens(s, s[i][j]+1, j)})"
|
||||
|
||||
#测试验证部分
|
||||
if __name__ == "__main__":
|
||||
# 经典测试案例
|
||||
p = [30, 35, 15, 5, 10, 20, 25]
|
||||
expected_result = 15125
|
||||
|
||||
# 计算最优解
|
||||
m, s = matrix_chain_order(p)
|
||||
n = len(p) - 1
|
||||
result = m[1][n]
|
||||
|
||||
# 输出测试结果
|
||||
print(f"矩阵维度数组: {p}")
|
||||
print(f"矩阵数量: {n}")
|
||||
print(f"计算得到的最小标量乘法次数: {result}")
|
||||
print(f"预期的最小标量乘法次数: {expected_result}")
|
||||
print(f"测试{'通过' if result == expected_result else '失败'}")
|
||||
print(f"最优括号化方案: {print_optimal_parens(s, 1, n)}")
|
||||
|
||||
# 额外测试案例
|
||||
p2 = [40, 20, 30, 10, 30]
|
||||
m2, s2 = matrix_chain_order(p2)
|
||||
print("\n第二个测试案例:")
|
||||
print(f"矩阵维度数组: {p2}")
|
||||
print(f"最小标量乘法次数: {m2[1][4]}") # 预期结果为 26000
|
||||
print(f"最优括号化方案: {print_optimal_parens(s2, 1, 4)}")
|
||||
|
||||
</code></pre>
|
||||
<h4>综合讨论</h4>
|
||||
<ul>
|
||||
<li>何时选“记忆化 Top-Down”,何时选“自底向上表格法”?</li>
|
||||
<li>如何从“纯递归”快速判断是否值得改造成 DP?</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
198
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-分位统计量.html
Normal file
198
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-分位统计量.html
Normal file
@@ -0,0 +1,198 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>分位统计量</h1>
|
||||
<h2>分位统计量</h2>
|
||||
<h3>第K大的数</h3>
|
||||
<p>在许多算法与数据分析任务中,我们不仅需要排序整个数组,还需要找出某个特定顺序统计量,例如:
|
||||
• 一个数组中第 K 大的元素;
|
||||
• 所有元素的中位数(K = n/2);
|
||||
• 某个分位点(25%、75%等)…
|
||||
<br>
|
||||
最直接的办法是先对数组进行排序,然后取第 K 个位置的元素。这种方法的时间复杂度为 <eq>O(n \log n)</eq>,因为我们需要对所有元素进行排序。
|
||||
<br>
|
||||
但我们真正关心的,只是一个位置的元素 —— 我们是否有更快的方法?
|
||||
<br></p>
|
||||
<h4>快速选择(QuickSelect)</h4>
|
||||
<p><strong>快排中的“轴枢”启发</strong>
|
||||
我们在快速排序中,每次选一个枢轴 pivot,通过划分操作把数组分成两部分:
|
||||
• 左边元素 <= pivot;
|
||||
• 右边元素 >= pivot。
|
||||
最终 pivot 会被放置固定位置上。
|
||||
<br>
|
||||
可见:</p>
|
||||
<blockquote>
|
||||
<p>若 pivot 恰好是我们要找的第 K 大元素,就不必继续递归。</p>
|
||||
</blockquote>
|
||||
<br>
|
||||
因此可得快速选择的具体做法,先令target等于N-K,即找第N-K+1小的数,其下标为target:
|
||||
1. 随机选一个枢轴;
|
||||
2. 利用快速排序的 partition 划分函数,找出 pivot 的位置 idx;
|
||||
3. 比较该位置与目标索引:
|
||||
• 若 idx == target,返回 pivot;
|
||||
• 若 idx > target,在左半边递归查找;
|
||||
• 否则在右半边递归查找。
|
||||
<br>
|
||||
理解做法并分析时间复杂度。
|
||||
### 快速选择的实现
|
||||
<p>实现一个 <code>quickselect(arr, k)</code> 函数,返回第 K 大的元素
|
||||
(也就是增序列下标N-K位置的数)</p>
|
||||
<pre><code class="language-python">import random
|
||||
|
||||
def partition(arr, low, high):
|
||||
"""
|
||||
对arr序列从low到high下标,找到一个枢轴,并返回其下标。(在arr中,比枢轴小的数都在idx左边,大的在右边)
|
||||
"""
|
||||
|
||||
def quickselect(arr, k):
|
||||
"""
|
||||
快速选择第K大元素(转换为第n-k小)
|
||||
:param arr: 输入数组
|
||||
:param k: 要找的第K大元素
|
||||
:return: 该元素值
|
||||
"""
|
||||
n = len(arr)
|
||||
target = n - k # 第k大转为第n-k小(序列下标)
|
||||
low, high = 0, n - 1
|
||||
|
||||
while low <= high:
|
||||
idx = partition(arr, low, high)
|
||||
if idx == target:
|
||||
return arr[idx]
|
||||
elif idx < target:
|
||||
low = idx + 1
|
||||
else:
|
||||
high = idx - 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
#测试1:随机数组
|
||||
random.seed(42) # 设置随机种子,确保结果可重现
|
||||
arr1 = [random.randint(1, 100) for _ in range(10)]
|
||||
k1 = 3
|
||||
result1 = quickselect(arr1.copy(), k1) # 使用副本避免修改原数组
|
||||
sorted_arr1 = sorted(arr1)
|
||||
expected1 = sorted_arr1[-k1]
|
||||
print(f"测试1 - 随机数组:")
|
||||
print(f"原数组: {arr1}")
|
||||
print(f"第{k1}大元素: {result1}, 预期值: {expected1}")
|
||||
print(f"测试{'通过' if result1 == expected1 else '失败'}\n")
|
||||
|
||||
#测试2:包含重复元素的数组
|
||||
arr2 = [5, 3, 8, 8, 1, 5, 9, 3, 5]
|
||||
k2 = 2
|
||||
result2 = quickselect(arr2.copy(), k2)
|
||||
sorted_arr2 = sorted(arr2)
|
||||
expected2 = sorted_arr2[-k2]
|
||||
print(f"测试2 - 包含重复元素:")
|
||||
print(f"原数组: {arr2}")
|
||||
print(f"第{k2}大元素: {result2}, 预期值: {expected2}")
|
||||
print(f"测试{'通过' if result2 == expected2 else '失败'}\n")
|
||||
|
||||
#测试3:边界条件 - k=1(最大元素)
|
||||
arr3 = [10, 20, 30, 40, 50]
|
||||
k3 = 1
|
||||
result3 = quickselect(arr3.copy(), k3)
|
||||
sorted_arr3 = sorted(arr3)
|
||||
expected3 = sorted_arr3[-k3]
|
||||
print(f"测试3 - 最大元素:")
|
||||
print(f"原数组: {arr3}")
|
||||
print(f"第{k3}大元素: {result3}, 预期值: {expected3}")
|
||||
print(f"测试{'通过' if result3 == expected3 else '失败'}\n")
|
||||
</code></pre>
|
||||
<h3>BFPRT 算法:中位数的中位数算法</h3>
|
||||
<p>快速选择的平均复杂度为 <eq>O(n)</eq>,但可能退化为 <eq>O(n^2)</eq>。</p>
|
||||
<h4>BFPRT分治法</h4>
|
||||
<p>BFPRT算法和二分查找中对“矩阵”进行分治的做法很像,也是通过矩阵的中心数,来排除一部分数据。
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" />
|
||||
<br>
|
||||
我们通过以下几个步骤确保选出的“轴枢”可以高概率地“削减”足够多的元素,从而保证递归过程每次至少减少 30% 左右的输入规模:
|
||||
<br></p>
|
||||
<ol>
|
||||
<li>将原始数组划分为若干个 5 个元素的组
|
||||
请看图中所有的竖列(粉红色背景小矩形):每个小矩形内有 5 个点,这就是我们将原始数据分成的小组。
|
||||
组的个数 ≈ n / 5(设 n 是总元素数量)
|
||||
不足 5 个的组,可以用一个全局最小值(如 -1)补全。
|
||||
<br></li>
|
||||
<li>对每组内部排序,取出中位数
|
||||
图中每个竖列的小组中,绿色圆点表示每组的元素。中间处于红框中的数为每组中位数。
|
||||
每列上两个数小于中位数,下两个大于。
|
||||
<br></li>
|
||||
<li>将所有组的中位数组成新数组,对其递归调用本算法找中位数。
|
||||
图中红色矩形框住的内容,正是“所有组中位数”的集合。这些点来自所有小组的中位点,组成新的长度为 n/5 的数组。
|
||||
对这个新数组再次使用本算法,递归找出它的中位数,作为我们最终要用的“枢轴”。即图中棕色点。
|
||||
<br></li>
|
||||
<li>这个中位数(棕色圆点)就是我们分治的“枢轴”
|
||||
有如下重要性质:
|
||||
a. 棕点及左上角(图中蓝框的点)均小于等于枢轴
|
||||
b. 棕点及右下角(图中黑框的点)均大于等于枢轴
|
||||
<br></li>
|
||||
<li>进行排除,分解,先令x=n-k+1;找第k大数就是找第x小数
|
||||
a. 若x>蓝框数数量,则在非蓝框数中找第k大
|
||||
b. 若k>黑框数数量,则在非黑框数中找第(k-黑框数数量)大
|
||||
c. 当k=1或n'(n'是当前子问题的数数量),或n'<=5,均可在不大于O(n')的用时完成找数。
|
||||
可以证明在c不成立的情况下,a,b两点至少成立一个。
|
||||
<br></li>
|
||||
</ol>
|
||||
<p>与AI教师讨论每一步在做的事和最终想要实现的效果。可以联想你之前学习到的二分查找中的矩阵分治。</p>
|
||||
<h3>BFPRT 复杂度分析</h3>
|
||||
<p>在理解了“中位数的中位数”算法的流程之后,我们接下来要正式分析它的<strong>时间复杂度</strong>,并证明该算法的整体运行时间为 <eq>O(n)</eq>。</p>
|
||||
<hr />
|
||||
<h4>写出递推式</h4>
|
||||
<p>根据 BFPRT 算法的执行过程,依次与AI教师讨论每步的时间复杂度:
|
||||
<br></p>
|
||||
<ol>
|
||||
<li><strong>将 <eq>n</eq> 个元素分组、找出每组中位数</strong>
|
||||
<ul>
|
||||
<li>分成 <eq>n/5</eq> 组,每组排序并取中位数。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>递归地找所有中位数的中位数(枢轴)</strong>
|
||||
<ul>
|
||||
<li>子问题规模为 <eq>n/5</eq>。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>额外一次线性 partition</strong>
|
||||
<ul>
|
||||
<li>对全数组按枢轴划分:蓝框部分、黑框部分,此外递归时是对非蓝框或非黑框的补集部分。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>将原始数组按枢轴划分,并在一侧继续递归</strong>
|
||||
<ul>
|
||||
<li>对非蓝框或非黑框数进行递归查询。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p><img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" /></p>
|
||||
<p>从而得到最终的递推式。</p>
|
||||
<hr />
|
||||
<h4>证明 <eq>T(n) = O(n)</eq></h4>
|
||||
<p>这里的证明并不复杂,使用高中学习的数学归纳法即可。
|
||||
提示:先假设满足T(n)<=cn;然后证明递推式<=d*cn<=cn,其中d小于1。</p>
|
||||
<p>与AI教师探讨证明流程。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
3899
Html/apps/static/cdnback/css/bootstrap-grid.css
vendored
Normal file
3899
Html/apps/static/cdnback/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/css/bootstrap-grid.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/css/bootstrap-grid.min.css
vendored
Normal file
7
Html/apps/static/cdnback/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/css/bootstrap-grid.min.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
327
Html/apps/static/cdnback/css/bootstrap-reboot.css
vendored
Normal file
327
Html/apps/static/cdnback/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2019 The Bootstrap Authors
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus:not(:focus-visible) {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[type="button"]:not(:disabled),
|
||||
[type="reset"]:not(:disabled),
|
||||
[type="submit"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
Html/apps/static/cdnback/css/bootstrap-reboot.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
Html/apps/static/cdnback/css/bootstrap-reboot.min.css
vendored
Normal file
8
Html/apps/static/cdnback/css/bootstrap-reboot.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2019 The Bootstrap Authors
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
File diff suppressed because one or more lines are too long
10224
Html/apps/static/cdnback/css/bootstrap.css
vendored
Normal file
10224
Html/apps/static/cdnback/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/css/bootstrap.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/css/bootstrap.min.css
vendored
Normal file
7
Html/apps/static/cdnback/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/css/bootstrap.min.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff
Normal file
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff2
Normal file
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
7134
Html/apps/static/cdnback/js/bootstrap.bundle.js
vendored
Normal file
7134
Html/apps/static/cdnback/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/js/bootstrap.bundle.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/js/bootstrap.bundle.min.js
vendored
Normal file
7
Html/apps/static/cdnback/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/js/bootstrap.bundle.min.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4521
Html/apps/static/cdnback/js/bootstrap.js
vendored
Normal file
4521
Html/apps/static/cdnback/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/js/bootstrap.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/js/bootstrap.min.js
vendored
Normal file
7
Html/apps/static/cdnback/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/js/bootstrap.min.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -336,18 +336,6 @@ body {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
/* 课程卡片样式 */
|
||||
.course-card {
|
||||
border: none;
|
||||
|
||||
@@ -19,19 +19,9 @@ body {
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
/* Header styles */
|
||||
header {
|
||||
@@ -88,25 +78,9 @@ main {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
|
||||
.select-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
@@ -115,11 +89,6 @@ main {
|
||||
.course-card:hover {
|
||||
transform: translateY(-10px); /* 提升效果 */
|
||||
}
|
||||
.course-card img {
|
||||
width: 100%;
|
||||
height: 261px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.course-card h3 {
|
||||
padding: 15px;
|
||||
@@ -139,9 +108,6 @@ main {
|
||||
}
|
||||
|
||||
.selected-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #8a8b8c;
|
||||
color: white;
|
||||
border: none;
|
||||
|
||||
281
Html/apps/static/css/learning-path.css
Normal file
281
Html/apps/static/css/learning-path.css
Normal file
@@ -0,0 +1,281 @@
|
||||
:root {
|
||||
--primary-blue: #6A9FB5;
|
||||
--secondary-blue: #8BB4C5;
|
||||
--light-blue: #E1F0F7;
|
||||
--accent-blue: #4A7B9D;
|
||||
--text-dark: #2C3E50;
|
||||
--text-light: #7F8C8D;
|
||||
--card-shadow: 0 4px 12px rgba(106, 159, 181, 0.15);
|
||||
}
|
||||
|
||||
/* 学习路径区域 */
|
||||
.learning-path-section {
|
||||
background: linear-gradient(135deg, var(--light-blue), #D4E9F2);
|
||||
padding: 2rem 0;
|
||||
border-radius: 0 0 20px 20px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--accent-blue);
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.section-title:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background-color: var(--primary-blue);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 专业选择器 */
|
||||
.major-selector {
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.major-selector label {
|
||||
font-weight: 600;
|
||||
color: var(--accent-blue);
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.major-selector select {
|
||||
appearance: none;
|
||||
background-color: white;
|
||||
border: 2px solid var(--primary-blue);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text-dark);
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.major-selector select:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(106, 159, 181, 0.3);
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.major-selector::after {
|
||||
content: '\f078';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
/* 学习路径流程图 */
|
||||
.path-flow {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.path-container {
|
||||
display: flex;
|
||||
min-width: 900px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.path-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: -50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: var(--primary-blue);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--light-blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step-node:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.step-node:hover .step-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.8rem;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.step-courses {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
height: 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--primary-blue);
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* 阶段状态样式 */
|
||||
.step-completed .step-node {
|
||||
background-color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.step-completed .step-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-active .step-node {
|
||||
background-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 4px rgba(74, 123, 157, 0.3);
|
||||
}
|
||||
|
||||
.step-active .step-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 课程详情面板 */
|
||||
.course-panel {
|
||||
display: none;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.course-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: 600;
|
||||
color: var(--accent-blue);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-panel {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.course-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.course-item:hover {
|
||||
border-color: var(--primary-blue);
|
||||
box-shadow: 0 2px 8px rgba(106, 159, 181, 0.15);
|
||||
}
|
||||
|
||||
.course-check {
|
||||
margin-right: 0.75rem;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.course-duration {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.course-action {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
/* 页眉导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #2575fc;
|
||||
color: rgb(235, 235, 235);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
background: linear-gradient(135deg, var(--primary-blue), var(--accent-blue));
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
|
||||
12
Html/apps/static/css/search-section.css
Normal file
12
Html/apps/static/css/search-section.css
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
BIN
Html/apps/static/favicon.ico
Normal file
BIN
Html/apps/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 738 KiB |
@@ -105,29 +105,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('chatbox').appendChild(requestMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
// for (let i = 0; i < data.length; i++) {
|
||||
// const element = data[i]
|
||||
// const requestMessage = document.createElement('div');
|
||||
// requestMessage.className = 'chat-message server-message';
|
||||
// const approveButton = document.createElement('button');
|
||||
// approveButton.innerText = '批准';
|
||||
// approveButton.data = element;
|
||||
// approveButton.style.width = '100%';
|
||||
// approveButton.style.borderRadius = '4px';
|
||||
// approveButton.onclick = function(event) {
|
||||
// console.log(element)
|
||||
// socket.emit('message', JSON.stringify({type:'function', data:element}));
|
||||
// event.currentTarget.disabled = true;
|
||||
// event.currentTarget.style.backgroundColor = 'gray';
|
||||
// event.currentTarget.style.color = 'white'; // 可选:设置文字颜色为白色,以便更清晰地显示
|
||||
// event.currentTarget.style.border = 'none'; // 可选:去掉边框
|
||||
// };
|
||||
// // 将按钮添加到消息气泡中
|
||||
// requestMessage.innerHTML = `<b>Function:</b> ${element.name}(${JSON.stringify(element.arguments)})<br>`;
|
||||
// requestMessage.appendChild(approveButton);
|
||||
// document.getElementById('chatbox').appendChild(requestMessage);
|
||||
// document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
// }
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -250,124 +228,9 @@ function sendMessage() {
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
|
||||
// let voice_stream = null;
|
||||
// let audio_chunks = [];
|
||||
// let isRecording = false;
|
||||
// let recordingInterval = null;
|
||||
// //鼠标点击终止事件
|
||||
// function handleMouseClick(event) {
|
||||
// event.stopPropagation();
|
||||
// stopRecording();
|
||||
// }
|
||||
// //发送语音
|
||||
// const startAudioRecording = async () => {
|
||||
// try {
|
||||
// const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// audio: {
|
||||
// sampleRate: 16000,
|
||||
// channelCount: 1,
|
||||
// echoCancellation: true,
|
||||
// noiseSuppression: true
|
||||
// }
|
||||
// });
|
||||
// const mediaRecorder = new MediaRecorder(stream);
|
||||
// const audioContext = new AudioContext({
|
||||
// sampleRate: 16000
|
||||
// });
|
||||
|
||||
// // Load the AudioWorklet
|
||||
// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
|
||||
|
||||
// const source = audioContext.createMediaStreamSource(stream);
|
||||
// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
|
||||
// let audioChunks = [];
|
||||
|
||||
// workletNode.port.onmessage = (e) => {
|
||||
// audioChunks.push(new Float32Array(e.data));
|
||||
|
||||
// if (audioChunks.length >= 16) {
|
||||
// // 合并所有音频块
|
||||
// const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
// const mergedData = new Float32Array(totalLength);
|
||||
// let offset = 0;
|
||||
// for (const chunk of audioChunks) {
|
||||
// mergedData.set(chunk, offset);
|
||||
// offset += chunk.length;
|
||||
// }
|
||||
|
||||
// const audioMessage = {
|
||||
// id: window.data.user_uuid,
|
||||
// type: 'audio',
|
||||
// data: mergedData,
|
||||
// sampleRate: 16000,
|
||||
// timestamp: Date.now()
|
||||
// };
|
||||
// socket.emit('voice', audioMessage);
|
||||
// console.log("voice sended")
|
||||
// audioChunks = [];
|
||||
// }
|
||||
// };
|
||||
|
||||
// source.connect(workletNode);
|
||||
// workletNode.connect(audioContext.destination);
|
||||
|
||||
// mediaRecorder.value = {
|
||||
// stop: () => {
|
||||
// source.disconnect();
|
||||
// workletNode.disconnect();
|
||||
// stream.getTracks().forEach(track => track.stop());
|
||||
// audioContext.close();
|
||||
// }
|
||||
// }
|
||||
|
||||
// isRecording.value = true;
|
||||
// } catch (error) {
|
||||
// console.error('Error starting audio recording:', error);
|
||||
// }
|
||||
// };
|
||||
// function sendVoiceMessage() {
|
||||
// startAudioRecording()
|
||||
// //变换录音中图标
|
||||
// document.getElementById('recordIcon').className = 'bi bi-record-circle';
|
||||
// document.addEventListener('click', handleMouseClick);
|
||||
// }
|
||||
|
||||
// //停止录音
|
||||
// function stopRecording() {
|
||||
// if (!isRecording) return;
|
||||
// isRecording = false;
|
||||
// //取消监听
|
||||
// document.removeEventListener('click', handleMouseClick);
|
||||
// //取消定时器
|
||||
// if (recordingInterval) {
|
||||
// clearInterval(recordingInterval);
|
||||
// recordingInterval = null;
|
||||
// }
|
||||
// //变换图标
|
||||
// document.getElementById('recordIcon').className = 'bi bi-mic';
|
||||
// //发送剩下的数据
|
||||
// if (audio_chunks.length > 0) {
|
||||
// const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0));
|
||||
// let offset = 0;
|
||||
// for (const chunk of audio_chunks) {
|
||||
// merged_data.set(new Uint8Array(chunk), offset);
|
||||
// offset += chunk.length;
|
||||
// }
|
||||
// socket.emit('VoiceMessage', merged_data, { binary: true });
|
||||
// audio_chunks = [];
|
||||
// }
|
||||
// overlay.style.display = 'none';
|
||||
// //关闭音频流
|
||||
// if (voice_stream) {
|
||||
// voice_stream.stop_stream();
|
||||
// voice_stream.close();
|
||||
// voice_stream = null;
|
||||
// }
|
||||
// if (p) {
|
||||
// p.terminate();
|
||||
// p = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
//语音这一块//
|
||||
|
||||
@@ -6,37 +6,36 @@ function show_course_details(course_id){
|
||||
window.location.href = '/course/' + course_id
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectButtons = document.querySelectorAll('.select-button');
|
||||
|
||||
selectButtons.forEach(button => {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.stopPropagation(); // 阻止事件冒泡
|
||||
const courseId = this.getAttribute('data-course-id');
|
||||
fetch(`/select_course`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ course_id: courseId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 更新按钮样式或显示消息
|
||||
this.classList.remove('select-button');
|
||||
this.classList.add('selected-button');
|
||||
this.textContent = '已选择';
|
||||
alert('选择课程成功');
|
||||
} else {
|
||||
alert('选择课程失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('选择课程失败');
|
||||
});
|
||||
});
|
||||
function on_click_select_course(event){
|
||||
event.stopPropagation(); // 阻止事件冒泡
|
||||
const courseId = event.currentTarget.getAttribute('data-course-id');
|
||||
if (event.currentTarget.classList.contains("selected-button")) {alert('课程已选择'); return;}
|
||||
fetch(`/select_course`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ course_id: courseId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 更新按钮样式或显示消息
|
||||
this.classList.remove('select-button');
|
||||
this.classList.add('selected-button');
|
||||
this.textContent = '已选择';
|
||||
alert('选择课程成功');
|
||||
} else {
|
||||
alert('选择课程失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('选择课程失败');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
});
|
||||
30
Html/apps/static/js/learning-path.js
Normal file
30
Html/apps/static/js/learning-path.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// 1. 点击学习阶段节点,显示对应课程面板
|
||||
document.querySelectorAll('.step-node').forEach((node) => {
|
||||
node.addEventListener('click', function () {
|
||||
// 隐藏所有面板
|
||||
document.querySelectorAll('.course-panel').forEach(panel => {
|
||||
panel.classList.remove('active');
|
||||
});
|
||||
// 获取当前阶段名称,匹配面板ID
|
||||
const stepTitle = this.closest('.path-step').querySelector('.step-title').textContent;
|
||||
const panelId = stepTitle.toLowerCase().replace(' ', '-') + '-panel';
|
||||
const targetPanel = document.getElementById(panelId);
|
||||
// 显示对应面板(若存在)
|
||||
if (targetPanel) {
|
||||
targetPanel.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 关闭课程面板
|
||||
document.querySelectorAll('.close-panel').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
this.closest('.course-panel').classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 切换专业方向(可扩展为动态更新学习路径)
|
||||
document.getElementById('majorSelect').addEventListener('change', function () {
|
||||
const selectedMajor = this.options[this.selectedIndex].text;
|
||||
// 实际项目中可替换为AJAX请求,加载对应专业的学习路径
|
||||
});
|
||||
@@ -10,6 +10,7 @@
|
||||
<!-- Font Awesome 图标 -->
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/search-section.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>华实学伴 - 专业的在线学习平台</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> -->
|
||||
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome 图标 -->
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/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/search-section.css">
|
||||
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
{% include 'learning-path.html' %} <!-- 引入learning-path.html -->
|
||||
<section class="search-section">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>课程选择主页</title>
|
||||
<link rel="stylesheet" href="/static/css/footer.css">
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<script src="/static/cdnback/jquery.min.js"></script>
|
||||
|
||||
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/search-section.css">
|
||||
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
</head>
|
||||
<body>
|
||||
{% include 'navbar.html' %}
|
||||
@@ -30,17 +36,42 @@
|
||||
|
||||
<div class="course-selection">
|
||||
<h2>课程列表</h2>
|
||||
<div class="course-list">
|
||||
<div class="course-cards" id="course-cards">
|
||||
{% for course in courses_data %}
|
||||
<div class="course-card" onclick="show_course_details('{{ course._id }}')">
|
||||
<img src="{{ course.image_url }}" alt="{{ course.course_name }}">
|
||||
<h3>{{ course.name }}</h3>
|
||||
<p>{{ course.description }}</p>
|
||||
{% if course._id in selected_courses %}
|
||||
<button class="selected-button">已选择</button>
|
||||
{% else %}
|
||||
<button class="select-button" data-course-id="{{course._id}}">选择课程</button>
|
||||
{% endif %}
|
||||
<!-- <div class="course-card" onclick="show_course_details('{{ course._id }}')">
|
||||
<img src="{{ course.image_url }}" alt="{{ course.course_name }}">
|
||||
<h3>{{ course.name }}</h3>
|
||||
<p>{{ course.description }}</p>
|
||||
{% if course._id in selected_courses %}
|
||||
<button class="selected-button">已选择</button>
|
||||
{% else %}
|
||||
<button class="select-button" data-course-id="{{course._id}}">选择课程</button>
|
||||
{% endif %}
|
||||
</div> -->
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')">
|
||||
<img src="{{course.image_url}}"
|
||||
class="course-img" alt="课程封面">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<span class="course-category">算法设计</span>
|
||||
<h5 class="course-title">{{course.name}}</h5>
|
||||
<p class="course-instructor">{{course.description}}</p>
|
||||
<div class="course-meta">
|
||||
<span><i class="far fa-user me-1"></i> 初级</span>
|
||||
<span class="course-rating"><i class="fas fa-star"></i> 4.7</span>
|
||||
</div>
|
||||
<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>
|
||||
{% if course._id in selected_courses %}
|
||||
<button class="btn btn-process flex-fill selected-button" data-course-id="{{course._id}}">已选课</button>
|
||||
{% else %}
|
||||
<button class="btn btn-process flex-fill select-button" onclick="on_click_select_course(event)" data-course-id="{{course._id}}">选择课程</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -53,7 +84,7 @@
|
||||
</body>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/"/g, "\""))
|
||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/'/g, "\""))
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/index.js"></script>
|
||||
|
||||
145
Html/apps/templates/learning-path.html
Normal file
145
Html/apps/templates/learning-path.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<link rel="stylesheet" href="/static/css/learning-path.css">
|
||||
<!-- 学习路径区域 -->
|
||||
<section class="learning-path-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">学习路径</h2>
|
||||
|
||||
<!-- 专业选择器 -->
|
||||
<div class="major-selector">
|
||||
<label for="majorSelect">选择专业方向:</label>
|
||||
<select class="form-select" id="majorSelect">
|
||||
<option value="computer">计算机科学与技术</option>
|
||||
<option value="data">数据科学与大数据</option>
|
||||
<option value="business">工商管理</option>
|
||||
<option value="design">艺术设计</option>
|
||||
<option value="language">语言文学</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 学习路径流程图 -->
|
||||
<div class="path-flow">
|
||||
<div class="path-container">
|
||||
<!-- 阶段1:已完成 -->
|
||||
<div class="path-step step-completed">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-code step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">编程基础</div>
|
||||
<div class="step-courses">3门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<div class="step-status">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段2:已完成 -->
|
||||
<div class="path-step step-completed">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-project-diagram step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">数据结构</div>
|
||||
<div class="step-courses">2门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<div class="step-status">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段3:进行中 -->
|
||||
<div class="path-step step-active">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-brain step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">算法设计</div>
|
||||
<div class="step-courses">3门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 67%"></div>
|
||||
</div>
|
||||
<div class="step-status">进行中</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段4:未开始 -->
|
||||
<div class="path-step">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-database step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">数据库原理</div>
|
||||
<div class="step-courses">2门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="step-status">未开始</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段5:未开始 -->
|
||||
<div class="path-step">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-laptop-code step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">Web开发</div>
|
||||
<div class="step-courses">5门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="step-status">未开始</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课程详情面板(默认隐藏,点击阶段显示) -->
|
||||
<div class="course-panel" id="algorithm-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">算法设计 - 课程列表</h3>
|
||||
<button class="close-panel"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="course-list">
|
||||
<div class="course-item">
|
||||
<i class="fas fa-check-circle course-check"></i>
|
||||
<div class="course-info">
|
||||
<div class="course-name">算法基础与复杂度分析</div>
|
||||
<div class="course-duration">12课时</div>
|
||||
</div>
|
||||
<div class="course-action">
|
||||
<button class="btn btn-sm btn-primary">继续学习</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-item">
|
||||
<i class="fas fa-check-circle course-check"></i>
|
||||
<div class="course-info">
|
||||
<div class="course-name">排序与搜索算法</div>
|
||||
<div class="course-duration">15课时</div>
|
||||
</div>
|
||||
<div class="course-action">
|
||||
<button class="btn btn-sm btn-primary">继续学习</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-item">
|
||||
<i class="far fa-circle course-check"></i>
|
||||
<div class="course-info">
|
||||
<div class="course-name">动态规划与贪心算法</div>
|
||||
<div class="course-duration">18课时</div>
|
||||
</div>
|
||||
<div class="course-action">
|
||||
<button class="btn btn-sm btn-outline-primary">开始学习</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/learning-path.js"></script>
|
||||
</section>
|
||||
@@ -1,8 +1,5 @@
|
||||
<link rel="stylesheet" href="/static/css/navbar.css">
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> -->
|
||||
<!-- Font Awesome 图标 -->
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> -->
|
||||
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<header class="navbar">
|
||||
|
||||
@@ -156,7 +156,6 @@ def load_chapters(markdown, markdown_prompt, score_prompt):
|
||||
|
||||
chapter_chain = []
|
||||
No = 1
|
||||
print (chapter_dict)
|
||||
for chapter_name in chapter_sequence:
|
||||
chapter_chain.append(Chapter( No, CHAPTER_LATTER, chapter_name, chapter_dict[chapter_name]["markdown"], chapter_dict[chapter_name]["markdown_prompt"], chapter_dict[chapter_name]["score_prompt"],chapter_dict[chapter_name]["require_tools"]))
|
||||
No+=1
|
||||
|
||||
@@ -4,6 +4,10 @@ from ..services.auth_service import register_user, login_user, logout_user
|
||||
from ..auth.decorators import require_role
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
# 将/favicon.ico重定向到/static/favicon.ico
|
||||
@bp.route('/favicon.ico')
|
||||
def redirect_favicon():
|
||||
return redirect(url_for('static', filename='favicon.ico'))
|
||||
|
||||
@bp.get("/register")
|
||||
def register():
|
||||
|
||||
@@ -43,13 +43,22 @@ class Backboard:
|
||||
f"- File tree: {self.file_tree}{endl}")
|
||||
# f"- Activated file content (current editing 10 lines): {endl}"
|
||||
def add_history(self, realtime_action):
|
||||
if(realtime_action['type']=='config'):return
|
||||
if len(self.history)!=0:
|
||||
if (self.history[-1]['type']=='fileEdit'):
|
||||
if(self.history[-1]['filePath'] == realtime_action['filePath']):
|
||||
# 检查必要的键是否存在,避免KeyError
|
||||
if realtime_action.get('type') == 'config':
|
||||
return
|
||||
if len(self.history) > 0 and realtime_action.get('type') == 'fileEdit':
|
||||
# 安全地获取filePath,并进行比较
|
||||
current_file_path = realtime_action.get('filePath')
|
||||
# 检查最近的历史记录是否包含filePath
|
||||
if current_file_path and self.history[-1].get('filePath') == current_file_path:
|
||||
# 安全地更新content,只有当content存在时才更新
|
||||
if 'content' in realtime_action:
|
||||
self.history[-1]['content'] = realtime_action['content']
|
||||
return
|
||||
return
|
||||
self.history.append(realtime_action)
|
||||
# 限制历史记录的数量
|
||||
if len(self.history) > 500: # 假设最多保留500条记录
|
||||
self.history.pop(0) # 移除最旧的记录
|
||||
|
||||
|
||||
def get_deltatime_mmss(self, start_time, end_time):
|
||||
|
||||
@@ -5,9 +5,9 @@ api_key = sk-aMpnWklN2IbsK44d1kNpy6YOP9bk1pdPJjFeEmbb0a5ytEFf
|
||||
model=gpt-4.1-nano
|
||||
|
||||
[VSCODE_WEB]
|
||||
url = https://hsamooc.com
|
||||
url = https://hsamooc.cn
|
||||
[CODE_LIKE]
|
||||
url = https://hsamooc.com/vsc-like
|
||||
url = /vsc-like
|
||||
#http://asengine.net:8282
|
||||
[VSCODE_WEB_PATH]
|
||||
is_wsl = true
|
||||
@@ -27,5 +27,6 @@ bucket = hsamooc-cdn-1374354408
|
||||
region = ap-guangzhou
|
||||
|
||||
[ASE_ENGINE]
|
||||
url = https://asengine.net
|
||||
namespace = /socket.io/c159a41d238b00f35ff33e036d007a0dec4d03197084b5557949a2f7f5ffce03
|
||||
url = https://test.asengine.net
|
||||
namespace = /socket.io/7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
|
||||
url_token = 7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
|
||||
|
||||
1
Html/db/data/user/_10235101550.json
Normal file
1
Html/db/data/user/_10235101550.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "_10235101550"}
|
||||
1
Html/db/data/user/a10235101450.json
Normal file
1
Html/db/data/user/a10235101450.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "a10235101450"}
|
||||
1
Html/db/data/user/ddd.json
Normal file
1
Html/db/data/user/ddd.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "ddd"}
|
||||
1
Html/db/data/user/dvd.json
Normal file
1
Html/db/data/user/dvd.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "dvd"}
|
||||
1
Html/db/data/user/emmeta.json
Normal file
1
Html/db/data/user/emmeta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "emmeta"}
|
||||
1
Html/db/data/user/exceptedgoat.json
Normal file
1
Html/db/data/user/exceptedgoat.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "exceptedgoat"}
|
||||
1
Html/db/data/user/huo.json
Normal file
1
Html/db/data/user/huo.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "huo"}
|
||||
1
Html/db/data/user/jqs.json
Normal file
1
Html/db/data/user/jqs.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "jqs"}
|
||||
1
Html/db/data/user/kktq.json
Normal file
1
Html/db/data/user/kktq.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "kktq"}
|
||||
1
Html/db/data/user/luyu.json
Normal file
1
Html/db/data/user/luyu.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "luyu"}
|
||||
1
Html/db/data/user/not_shy_10235101481.json
Normal file
1
Html/db/data/user/not_shy_10235101481.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "not_shy_10235101481"}
|
||||
1
Html/db/data/user/sjm666.json
Normal file
1
Html/db/data/user/sjm666.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "sjm666"}
|
||||
1
Html/db/data/user/tian.json
Normal file
1
Html/db/data/user/tian.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "tian"}
|
||||
1
Html/db/data/user/wawacai.json
Normal file
1
Html/db/data/user/wawacai.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "wawacai"}
|
||||
1
Html/db/data/user/yy.json
Normal file
1
Html/db/data/user/yy.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "yy"}
|
||||
2
Html/nohup.out
Normal file
2
Html/nohup.out
Normal file
@@ -0,0 +1,2 @@
|
||||
[2025-11-13 20:27:29,577] INFO in __init__: Shutting down gracefully (2)...
|
||||
[2025-11-13 20:27:29,578] INFO in __init__: Cleanup finished (2).
|
||||
@@ -148,12 +148,14 @@
|
||||
});
|
||||
|
||||
// 防抖处理非粘贴的编辑操作
|
||||
const content = changes.text;
|
||||
// 确保始终使用完整的编辑器内容而非仅变更部分
|
||||
const fullContent = editor.getValue();
|
||||
debounceSendToServer({
|
||||
type: 'fileEdit',
|
||||
filePath: filePath,
|
||||
content: content,
|
||||
content: fullContent,
|
||||
config: code_like_config,
|
||||
timestamp: new Date().getTime() // 添加时间戳以便追踪
|
||||
}, 5000
|
||||
);
|
||||
// saveFile
|
||||
|
||||
Reference in New Issue
Block a user