更新内容: - 后端 AI 服务优化(能力分析、知识点解析等) - 前端考试和陪练界面更新 - 修复多个 prompt 和 JSON 解析问题 - 更新 Coze 语音客户端
This commit is contained in:
23
admin-frontend/.eslintrc.cjs
Normal file
23
admin-frontend/.eslintrc.cjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Vue 组件命名规范
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
// 允许使用 any 类型(逐步改进)
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
// 未使用变量警告
|
||||||
|
'@typescript-eslint/no-unused-vars': 'warn'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -17,3 +17,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 519 B |
@@ -64,3 +64,8 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ async def submit_exam(
|
|||||||
@router.get("/mistakes", response_model=ResponseModel[GetMistakesResponse])
|
@router.get("/mistakes", response_model=ResponseModel[GetMistakesResponse])
|
||||||
async def get_mistakes(
|
async def get_mistakes(
|
||||||
exam_id: int,
|
exam_id: int,
|
||||||
|
round: int = Query(None, description="获取指定轮次的错题,不传则获取所有轮次"),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
@@ -149,18 +150,31 @@ async def get_mistakes(
|
|||||||
|
|
||||||
用于第二、三轮考试时获取上一轮的错题记录
|
用于第二、三轮考试时获取上一轮的错题记录
|
||||||
返回的数据可直接序列化为JSON字符串作为mistake_records参数传给考试生成接口
|
返回的数据可直接序列化为JSON字符串作为mistake_records参数传给考试生成接口
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
- exam_id: 考试ID
|
||||||
|
- round: 指定轮次(1/2/3),用于获取特定轮次的错题
|
||||||
|
第2轮考试时传入round=1,获取第1轮的错题
|
||||||
|
第3轮考试时传入round=2,获取第2轮的错题
|
||||||
|
不传则获取该考试的所有错题
|
||||||
"""
|
"""
|
||||||
logger.info(f"📋 GET /mistakes 收到请求")
|
logger.info(f"📋 GET /mistakes 收到请求")
|
||||||
try:
|
try:
|
||||||
logger.info(f"📋 获取错题记录 - exam_id: {exam_id}, user_id: {current_user.id}")
|
logger.info(f"📋 获取错题记录 - exam_id: {exam_id}, round: {round}, user_id: {current_user.id}")
|
||||||
|
|
||||||
# 查询指定考试的错题记录
|
# 构建查询条件
|
||||||
result = await db.execute(
|
query = select(ExamMistake).where(
|
||||||
select(ExamMistake).where(
|
ExamMistake.exam_id == exam_id,
|
||||||
ExamMistake.exam_id == exam_id,
|
ExamMistake.user_id == current_user.id,
|
||||||
ExamMistake.user_id == current_user.id,
|
|
||||||
).order_by(ExamMistake.id)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 如果指定了轮次,只获取该轮次的错题
|
||||||
|
if round is not None:
|
||||||
|
query = query.where(ExamMistake.round == round)
|
||||||
|
|
||||||
|
query = query.order_by(ExamMistake.id)
|
||||||
|
|
||||||
|
result = await db.execute(query)
|
||||||
mistakes = result.scalars().all()
|
mistakes = result.scalars().all()
|
||||||
|
|
||||||
logger.info(f"✅ 查询到错题记录数量: {len(mistakes)}")
|
logger.info(f"✅ 查询到错题记录数量: {len(mistakes)}")
|
||||||
@@ -262,7 +276,18 @@ async def generate_exam(
|
|||||||
- 第一轮考试:mistake_records 传空或不传
|
- 第一轮考试:mistake_records 传空或不传
|
||||||
- 第二、三轮错题重考:mistake_records 传入上一轮错题记录的JSON字符串
|
- 第二、三轮错题重考:mistake_records 传入上一轮错题记录的JSON字符串
|
||||||
"""
|
"""
|
||||||
|
from app.models.course import Course
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 验证课程存在性
|
||||||
|
course = await db.get(Course, request.course_id)
|
||||||
|
if not course or course.is_deleted:
|
||||||
|
logger.warning(f"课程不存在或已删除: course_id={request.course_id}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"无法生成试题:课程ID {request.course_id} 不存在或已被删除"
|
||||||
|
)
|
||||||
|
|
||||||
# 从用户信息中自动获取岗位ID(如果未提供)
|
# 从用户信息中自动获取岗位ID(如果未提供)
|
||||||
position_id = request.position_id
|
position_id = request.position_id
|
||||||
if not position_id:
|
if not position_id:
|
||||||
@@ -486,6 +511,7 @@ async def record_mistake(
|
|||||||
mistake = ExamMistake(
|
mistake = ExamMistake(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
exam_id=request.exam_id,
|
exam_id=request.exam_id,
|
||||||
|
round=request.round, # 记录考试轮次
|
||||||
question_id=request.question_id,
|
question_id=request.question_id,
|
||||||
knowledge_point_id=None, # 暂时设为None,避免外键约束
|
knowledge_point_id=None, # 暂时设为None,避免外键约束
|
||||||
question_content=request.question_content,
|
question_content=request.question_content,
|
||||||
@@ -503,7 +529,7 @@ async def record_mistake(
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"记录错题成功 - user_id: {current_user.id}, exam_id: {request.exam_id}, "
|
f"记录错题成功 - user_id: {current_user.id}, exam_id: {request.exam_id}, "
|
||||||
f"mistake_id: {mistake.id}"
|
f"round: {request.round}, mistake_id: {mistake.id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return ResponseModel(
|
return ResponseModel(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ExamMistake(BaseModel):
|
|||||||
# 核心关联字段
|
# 核心关联字段
|
||||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="用户ID")
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="用户ID")
|
||||||
exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True, comment="考试ID")
|
exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True, comment="考试ID")
|
||||||
|
round = Column(Integer, nullable=True, default=1, comment="考试轮次(1/2/3)")
|
||||||
question_id = Column(Integer, ForeignKey("questions.id", ondelete="SET NULL"), nullable=True, index=True, comment="题目ID(AI生成的题目可能为空)")
|
question_id = Column(Integer, ForeignKey("questions.id", ondelete="SET NULL"), nullable=True, index=True, comment="题目ID(AI生成的题目可能为空)")
|
||||||
knowledge_point_id = Column(Integer, ForeignKey("knowledge_points.id", ondelete="SET NULL"), nullable=True, index=True, comment="关联的知识点ID")
|
knowledge_point_id = Column(Integer, ForeignKey("knowledge_points.id", ondelete="SET NULL"), nullable=True, index=True, comment="关联的知识点ID")
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class JudgeAnswerResponse(BaseModel):
|
|||||||
class RecordMistakeRequest(BaseModel):
|
class RecordMistakeRequest(BaseModel):
|
||||||
"""记录错题请求"""
|
"""记录错题请求"""
|
||||||
exam_id: int = Field(..., description="考试ID")
|
exam_id: int = Field(..., description="考试ID")
|
||||||
|
round: int = Field(1, description="考试轮次(1/2/3)")
|
||||||
question_id: Optional[int] = Field(None, description="题目ID(AI生成的题目可能为空)")
|
question_id: Optional[int] = Field(None, description="题目ID(AI生成的题目可能为空)")
|
||||||
knowledge_point_id: Optional[int] = Field(None, description="知识点ID")
|
knowledge_point_id: Optional[int] = Field(None, description="知识点ID")
|
||||||
question_content: str = Field(..., description="题目内容")
|
question_content: str = Field(..., description="题目内容")
|
||||||
|
|||||||
@@ -477,3 +477,8 @@ ability_analysis_service = AbilityAnalysisService()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,10 @@ class CourseChatServiceV2:
|
|||||||
|
|
||||||
# 6. 更新会话索引
|
# 6. 更新会话索引
|
||||||
if is_new_conversation:
|
if is_new_conversation:
|
||||||
await self._add_to_conversation_index(user_id, conversation_id, course_id)
|
await self._add_to_conversation_index(
|
||||||
|
user_id, conversation_id, course_id,
|
||||||
|
first_message=query # 使用用户第一条消息作为会话名称
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self._update_conversation_index(user_id, conversation_id)
|
await self._update_conversation_index(user_id, conversation_id)
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ class CourseChatServiceV2:
|
|||||||
Tuple[str, Optional[str]]: (事件类型, 数据)
|
Tuple[str, Optional[str]]: (事件类型, 数据)
|
||||||
- ("conversation_started", conversation_id): 会话开始
|
- ("conversation_started", conversation_id): 会话开始
|
||||||
- ("chunk", text): 文本块
|
- ("chunk", text): 文本块
|
||||||
- ("end", None): 结束
|
- ("done", full_answer): 结束,附带完整回答
|
||||||
- ("error", message): 错误
|
- ("error", message): 错误
|
||||||
"""
|
"""
|
||||||
full_answer = ""
|
full_answer = ""
|
||||||
@@ -251,7 +254,7 @@ class CourseChatServiceV2:
|
|||||||
yield ("chunk", chunk)
|
yield ("chunk", chunk)
|
||||||
|
|
||||||
# 6. 发送结束事件
|
# 6. 发送结束事件
|
||||||
yield ("end", None)
|
yield ("done", full_answer)
|
||||||
|
|
||||||
# 7. 保存对话历史
|
# 7. 保存对话历史
|
||||||
await self._save_conversation_history(
|
await self._save_conversation_history(
|
||||||
@@ -262,7 +265,10 @@ class CourseChatServiceV2:
|
|||||||
|
|
||||||
# 8. 更新会话索引
|
# 8. 更新会话索引
|
||||||
if is_new_conversation:
|
if is_new_conversation:
|
||||||
await self._add_to_conversation_index(user_id, conversation_id, course_id)
|
await self._add_to_conversation_index(
|
||||||
|
user_id, conversation_id, course_id,
|
||||||
|
first_message=query # 使用用户第一条消息作为会话名称
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self._update_conversation_index(user_id, conversation_id)
|
await self._update_conversation_index(user_id, conversation_id)
|
||||||
|
|
||||||
@@ -526,7 +532,8 @@ class CourseChatServiceV2:
|
|||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
conversation_id: str,
|
conversation_id: str,
|
||||||
course_id: int
|
course_id: int,
|
||||||
|
first_message: str = ""
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
将会话添加到用户索引
|
将会话添加到用户索引
|
||||||
@@ -535,6 +542,7 @@ class CourseChatServiceV2:
|
|||||||
user_id: 用户ID
|
user_id: 用户ID
|
||||||
conversation_id: 会话ID
|
conversation_id: 会话ID
|
||||||
course_id: 课程ID
|
course_id: 课程ID
|
||||||
|
first_message: 用户第一条消息(用于生成会话名称)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from app.core.redis import get_redis_client
|
from app.core.redis import get_redis_client
|
||||||
@@ -547,12 +555,21 @@ class CourseChatServiceV2:
|
|||||||
await redis.zadd(index_key, {conversation_id: timestamp})
|
await redis.zadd(index_key, {conversation_id: timestamp})
|
||||||
await redis.expire(index_key, CONVERSATION_INDEX_TTL)
|
await redis.expire(index_key, CONVERSATION_INDEX_TTL)
|
||||||
|
|
||||||
# 2. 保存会话元数据
|
# 2. 生成会话名称(取用户第一条消息的前30个字符)
|
||||||
|
conversation_name = ""
|
||||||
|
if first_message:
|
||||||
|
# 移除换行符,截取前30个字符
|
||||||
|
conversation_name = first_message.replace('\n', ' ').strip()[:30]
|
||||||
|
if len(first_message) > 30:
|
||||||
|
conversation_name += "..."
|
||||||
|
|
||||||
|
# 3. 保存会话元数据(包含会话名称)
|
||||||
meta_key = f"{CONVERSATION_META_PREFIX}{conversation_id}"
|
meta_key = f"{CONVERSATION_META_PREFIX}{conversation_id}"
|
||||||
meta_data = {
|
meta_data = {
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"course_id": course_id,
|
"course_id": course_id,
|
||||||
|
"name": conversation_name, # 会话名称
|
||||||
"created_at": timestamp,
|
"created_at": timestamp,
|
||||||
"updated_at": timestamp,
|
"updated_at": timestamp,
|
||||||
}
|
}
|
||||||
@@ -563,7 +580,8 @@ class CourseChatServiceV2:
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"会话已添加到索引 - user_id: {user_id}, conversation_id: {conversation_id}"
|
f"会话已添加到索引 - user_id: {user_id}, conversation_id: {conversation_id}, "
|
||||||
|
f"name: {conversation_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -671,8 +689,10 @@ class CourseChatServiceV2:
|
|||||||
"updated_at": time.time(),
|
"updated_at": time.time(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 获取最后一条消息作为预览
|
# 获取历史消息
|
||||||
history = await self._get_conversation_history(conv_id)
|
history = await self._get_conversation_history(conv_id)
|
||||||
|
|
||||||
|
# 获取最后一条消息作为预览
|
||||||
last_message = ""
|
last_message = ""
|
||||||
if history:
|
if history:
|
||||||
# 获取最后一条 assistant 消息
|
# 获取最后一条 assistant 消息
|
||||||
@@ -683,8 +703,21 @@ class CourseChatServiceV2:
|
|||||||
last_message += "..."
|
last_message += "..."
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# 获取会话名称
|
||||||
|
# 优先使用元数据中保存的名称,如果没有则从历史消息中提取
|
||||||
|
conversation_name = meta.get("name", "")
|
||||||
|
if not conversation_name and history:
|
||||||
|
# 从第一条用户消息生成名称
|
||||||
|
for msg in history:
|
||||||
|
if msg["role"] == "user":
|
||||||
|
conversation_name = msg["content"].replace('\n', ' ').strip()[:30]
|
||||||
|
if len(msg["content"]) > 30:
|
||||||
|
conversation_name += "..."
|
||||||
|
break
|
||||||
|
|
||||||
conversations.append({
|
conversations.append({
|
||||||
"id": conv_id,
|
"id": conv_id,
|
||||||
|
"name": conversation_name, # 添加会话名称字段
|
||||||
"course_id": meta.get("course_id"),
|
"course_id": meta.get("course_id"),
|
||||||
"created_at": meta.get("created_at"),
|
"created_at": meta.get("created_at"),
|
||||||
"updated_at": meta.get("updated_at"),
|
"updated_at": meta.get("updated_at"),
|
||||||
|
|||||||
@@ -510,3 +510,8 @@ async def generate_exam(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -546,3 +546,8 @@ knowledge_analysis_service_v2 = KnowledgeAnalysisServiceV2()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,16 @@ def _preprocess_text(text: str) -> str:
|
|||||||
# 移除零宽字符
|
# 移除零宽字符
|
||||||
text = re.sub(r'[\u200b\u200c\u200d\ufeff]', '', text)
|
text = re.sub(r'[\u200b\u200c\u200d\ufeff]', '', text)
|
||||||
|
|
||||||
|
# 【重要】先替换中文标点为英文标点(在找边界之前做,否则中文引号会破坏边界检测)
|
||||||
|
cn_punctuation = {
|
||||||
|
',': ',', '。': '.', ':': ':', ';': ';',
|
||||||
|
'"': '"', '"': '"', ''': "'", ''': "'",
|
||||||
|
'【': '[', '】': ']', '(': '(', ')': ')',
|
||||||
|
'{': '{', '}': '}',
|
||||||
|
}
|
||||||
|
for cn, en in cn_punctuation.items():
|
||||||
|
text = text.replace(cn, en)
|
||||||
|
|
||||||
# 提取 Markdown 代码块
|
# 提取 Markdown 代码块
|
||||||
patterns = [
|
patterns = [
|
||||||
r'```json\s*([\s\S]*?)\s*```',
|
r'```json\s*([\s\S]*?)\s*```',
|
||||||
@@ -705,3 +715,4 @@ def clean_llm_output(text: str) -> Tuple[str, List[str]]:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -377,3 +377,8 @@ async def prepare_practice_knowledge(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -213,3 +213,8 @@ PRIORITY_LEVELS = ["high", "medium", "low"]
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,3 +46,8 @@ INCORRECT_KEYWORDS = ["错误", "incorrect", "false", "no", "wrong", "不正确"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,8 @@ DEFAULT_TEMPERATURE = 0.7
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -254,8 +254,11 @@ QUESTION_SCHEMA = {
|
|||||||
"description": "知识点ID"
|
"description": "知识点ID"
|
||||||
},
|
},
|
||||||
"correct": {
|
"correct": {
|
||||||
"type": "string",
|
"oneOf": [
|
||||||
"description": "正确答案"
|
{"type": "string"},
|
||||||
|
{"type": "array", "items": {"type": "string"}}
|
||||||
|
],
|
||||||
|
"description": "正确答案(单选/判断/填空为字符串,多选为数组)"
|
||||||
},
|
},
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -298,3 +301,8 @@ MAX_DIFFICULTY_LEVEL = 5
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -146,3 +146,8 @@ DEFAULT_KNOWLEDGE_TYPE = "理论知识"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -191,3 +191,8 @@ ANNOTATION_TAGS = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -190,3 +190,8 @@ DEFAULT_DIFFICULTY = "intermediate"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,8 @@ watchfiles==0.21.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ CREATE TABLE IF NOT EXISTS exam_mistakes (
|
|||||||
-- 核心关联字段(必需)
|
-- 核心关联字段(必需)
|
||||||
user_id INT NOT NULL COMMENT '用户ID',
|
user_id INT NOT NULL COMMENT '用户ID',
|
||||||
exam_id INT NOT NULL COMMENT '考试ID',
|
exam_id INT NOT NULL COMMENT '考试ID',
|
||||||
|
round INT DEFAULT 1 COMMENT '考试轮次(1/2/3)', -- 2026-01-27新增:标识错题产生于哪一轮
|
||||||
question_id INT COMMENT '题目ID(AI生成的题目可能为空)',
|
question_id INT COMMENT '题目ID(AI生成的题目可能为空)',
|
||||||
knowledge_point_id INT COMMENT '关联的知识点ID',
|
knowledge_point_id INT COMMENT '关联的知识点ID',
|
||||||
|
|
||||||
@@ -356,14 +357,15 @@ CREATE TABLE IF NOT EXISTS exam_mistakes (
|
|||||||
|
|
||||||
INDEX idx_user_id (user_id),
|
INDEX idx_user_id (user_id),
|
||||||
INDEX idx_exam_id (exam_id),
|
INDEX idx_exam_id (exam_id),
|
||||||
|
INDEX idx_exam_round (exam_id, round), -- 2026-01-27新增:支持按考试和轮次查询
|
||||||
INDEX idx_knowledge_point_id (knowledge_point_id),
|
INDEX idx_knowledge_point_id (knowledge_point_id),
|
||||||
INDEX idx_question_type (question_type) -- 2025-10-12新增
|
INDEX idx_question_type (question_type) -- 2025-10-12新增
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='错题记录表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='错题记录表';
|
||||||
```
|
```
|
||||||
|
|
||||||
**核心字段说明:**
|
**核心字段说明:**
|
||||||
- 包含8个核心字段:`user_id`、`exam_id`、`question_id`、`knowledge_point_id`、`question_content`、`correct_answer`、`user_answer`、`question_type`
|
- 包含9个核心字段:`user_id`、`exam_id`、`round`、`question_id`、`knowledge_point_id`、`question_content`、`correct_answer`、`user_answer`、`question_type`
|
||||||
- 简化设计,去除冗余字段(如错误次数、掌握状态等),聚焦核心功能
|
- `round` 字段(2026-01-27新增):标识错题产生于哪一轮考试,用于多轮错题重考时只获取上一轮的错题
|
||||||
- `question_id` 可为空:AI动态生成的题目可能不在 questions 表中
|
- `question_id` 可为空:AI动态生成的题目可能不在 questions 表中
|
||||||
- `knowledge_point_id` 可为空:用于关联知识点,支持错题重考功能
|
- `knowledge_point_id` 可为空:用于关联知识点,支持错题重考功能
|
||||||
- `question_type` 用于记录题型,支持错题按题型筛选和统计(2025-10-12新增)
|
- `question_type` 用于记录题型,支持错题按题型筛选和统计(2025-10-12新增)
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export interface JudgeAnswerResponse {
|
|||||||
*/
|
*/
|
||||||
export interface RecordMistakeRequest {
|
export interface RecordMistakeRequest {
|
||||||
exam_id: number
|
exam_id: number
|
||||||
|
round: number // 考试轮次(1/2/3)
|
||||||
question_id?: number | null
|
question_id?: number | null
|
||||||
knowledge_point_id?: number | null
|
knowledge_point_id?: number | null
|
||||||
question_content: string
|
question_content: string
|
||||||
@@ -154,9 +155,18 @@ export function recordMistake(data: RecordMistakeRequest) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取错题记录
|
* 获取错题记录
|
||||||
|
* @param examId 考试ID
|
||||||
|
* @param round 指定轮次(可选),用于获取特定轮次的错题
|
||||||
|
* 第2轮考试时传入round=1,获取第1轮的错题
|
||||||
|
* 第3轮考试时传入round=2,获取第2轮的错题
|
||||||
|
* 不传则获取该考试的所有错题
|
||||||
*/
|
*/
|
||||||
export function getMistakes(examId: number) {
|
export function getMistakes(examId: number, round?: number) {
|
||||||
return http.get<GetMistakesResponse>(`/api/v1/exams/mistakes?exam_id=${examId}`, {
|
let url = `/api/v1/exams/mistakes?exam_id=${examId}`
|
||||||
|
if (round !== undefined) {
|
||||||
|
url += `&round=${round}`
|
||||||
|
}
|
||||||
|
return http.get<GetMistakesResponse>(url, {
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,15 +89,16 @@ export class CozeVoiceClient {
|
|||||||
console.log('[CozeVoice] ✅ 已设置播放音量=1')
|
console.log('[CozeVoice] ✅ 已设置播放音量=1')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 如果有场景提示词,发送初始消息
|
// ⚠️ 重要:不要在连接后发送场景提示词!
|
||||||
// 注意:参考代码不发送初始消息,而是等待用户先说话
|
// SDK连接时默认设置 need_play_prologue: true,Bot会自动播放开场白
|
||||||
// 但我们的场景需要AI先开场白,所以发送场景提示词
|
// 如果手动发送sendTextMessage会干扰开场白播放流程
|
||||||
|
// 场景信息应该在Bot的系统提示词中预设
|
||||||
if (config.scenePrompt) {
|
if (config.scenePrompt) {
|
||||||
this.client.sendTextMessage(config.scenePrompt)
|
console.log('[CozeVoice] ⚠️ 场景提示词将通过Bot开场白播放,不手动发送')
|
||||||
console.log('[CozeVoice] ✅ 场景提示词已发送')
|
console.log('[CozeVoice] 📝 场景提示词:', config.scenePrompt.substring(0, 100) + '...')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[CozeVoice] 🎉 初始化完成,等待对话...')
|
console.log('[CozeVoice] 🎉 初始化完成,等待Bot开场白和用户对话...')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[CozeVoice] ❌ 连接失败:', error)
|
console.error('[CozeVoice] ❌ 连接失败:', error)
|
||||||
@@ -146,6 +147,10 @@ export class CozeVoiceClient {
|
|||||||
|
|
||||||
if (!event) return
|
if (!event) return
|
||||||
|
|
||||||
|
// 🔍 调试:打印所有收到的事件(详细日志)
|
||||||
|
console.log('[CozeVoice] 📨 收到事件:', eventName, '-> event_type:', event.event_type)
|
||||||
|
console.log('[CozeVoice] 📨 事件数据:', JSON.stringify(event.data || {}).substring(0, 200))
|
||||||
|
|
||||||
switch (event.event_type) {
|
switch (event.event_type) {
|
||||||
// 用户开始说话 - 关闭扬声器本地回放(避免用户听到自己的声音)
|
// 用户开始说话 - 关闭扬声器本地回放(避免用户听到自己的声音)
|
||||||
case 'input_audio_buffer.speech_started': {
|
case 'input_audio_buffer.speech_started': {
|
||||||
@@ -221,6 +226,43 @@ export class CozeVoiceClient {
|
|||||||
this.emit('ai_speech_end', {})
|
this.emit('ai_speech_end', {})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔍 调试:捕获服务器错误事件
|
||||||
|
case 'error': {
|
||||||
|
console.error('[CozeVoice] ❌ 服务器返回错误:', event)
|
||||||
|
this.emit('error', { error: event.data?.error || '未知错误' })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话创建事件
|
||||||
|
case WebsocketsEventType.CONVERSATION_CHAT_CREATED: {
|
||||||
|
console.log('[CozeVoice] ✅ 对话创建成功')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话进行中
|
||||||
|
case 'conversation.chat.in_progress': {
|
||||||
|
console.log('[CozeVoice] 🔄 AI正在思考...')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话失败事件
|
||||||
|
case 'conversation.chat.failed': {
|
||||||
|
console.error('[CozeVoice] ❌ 对话失败:', event)
|
||||||
|
this.emit('error', { error: event.data?.last_error?.msg || '对话失败' })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话完成事件
|
||||||
|
case 'conversation.chat.completed': {
|
||||||
|
console.log('[CozeVoice] ✅ 对话完成')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认处理:记录未知事件
|
||||||
|
default: {
|
||||||
|
console.log('[CozeVoice] ⚠️ 未处理的事件类型:', event.event_type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,16 @@ const formatDuration = (seconds: number) => {
|
|||||||
return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`
|
return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期为 YYYY-MM-DD(使用本地时间,避免时区问题)
|
||||||
|
*/
|
||||||
|
const formatLocalDate = (date: Date): string => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载成绩报告数据
|
* 加载成绩报告数据
|
||||||
*/
|
*/
|
||||||
@@ -263,10 +273,10 @@ const loadReportData = async () => {
|
|||||||
try {
|
try {
|
||||||
const params: any = {}
|
const params: any = {}
|
||||||
|
|
||||||
// 添加时间范围参数
|
// 添加时间范围参数(使用本地时间格式化,避免UTC时区偏移问题)
|
||||||
if (dateRange.value && dateRange.value.length === 2) {
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
params.start_date = dateRange.value[0].toISOString().split('T')[0]
|
params.start_date = formatLocalDate(dateRange.value[0])
|
||||||
params.end_date = dateRange.value[1].toISOString().split('T')[0]
|
params.end_date = formatLocalDate(dateRange.value[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getExamReport(params)
|
const response = await getExamReport(params)
|
||||||
|
|||||||
@@ -341,8 +341,28 @@ const transformDifyQuestions = (difyQuestions: any[]): any[] => {
|
|||||||
const options = q.topic?.options || {}
|
const options = q.topic?.options || {}
|
||||||
// 解析正确答案:支持 "A"、"A,B"、"A、B"、"A:xxx" 等多种格式
|
// 解析正确答案:支持 "A"、"A,B"、"A、B"、"A:xxx" 等多种格式
|
||||||
const correctAnswerStr = String(q.correct || '')
|
const correctAnswerStr = String(q.correct || '')
|
||||||
// 提取正确答案中的选项字母(A、B、C、D等)
|
|
||||||
const correctLetters = correctAnswerStr.match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || []
|
// 更精确地提取正确答案字母(避免误提取选项内容中的字母)
|
||||||
|
let correctLetters: string[] = []
|
||||||
|
|
||||||
|
// 情况1:纯选项字母格式(如 "A", "B", "A,B", "A、B", "A,B,C")
|
||||||
|
const pureLetterMatch = correctAnswerStr.trim().match(/^[A-Da-d]([,、\s]+[A-Da-d])*$/)
|
||||||
|
if (pureLetterMatch) {
|
||||||
|
correctLetters = correctAnswerStr.match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || []
|
||||||
|
} else {
|
||||||
|
// 情况2:选项字母+内容格式(如 "A:xxx" 或 "A: xxx")
|
||||||
|
// 只提取开头的选项字母(可能有多个,如 "A,C:xxx")
|
||||||
|
const prefixMatch = correctAnswerStr.match(/^([A-Da-d](?:[,、\s]*[A-Da-d])*)[:::\s]/)
|
||||||
|
if (prefixMatch) {
|
||||||
|
correctLetters = prefixMatch[1].match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || []
|
||||||
|
} else {
|
||||||
|
// 情况3:只有开头字母(如 "A" 后面直接跟内容)
|
||||||
|
const firstLetterMatch = correctAnswerStr.match(/^([A-Da-d])/)
|
||||||
|
if (firstLetterMatch) {
|
||||||
|
correctLetters = [firstLetterMatch[1].toUpperCase()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`🔍 解析题目[${index + 1}] - correct: "${q.correct}", correctLetters: [${correctLetters.join(', ')}]`)
|
console.log(`🔍 解析题目[${index + 1}] - correct: "${q.correct}", correctLetters: [${correctLetters.join(', ')}]`)
|
||||||
|
|
||||||
@@ -481,10 +501,11 @@ const loadQuestions = async () => {
|
|||||||
// 第一轮考试不传mistake_records参数
|
// 第一轮考试不传mistake_records参数
|
||||||
// 第二、三轮考试传入上一轮的错题记录
|
// 第二、三轮考试传入上一轮的错题记录
|
||||||
if (currentRound.value > 1) {
|
if (currentRound.value > 1) {
|
||||||
// 用同一个exam_id获取错题
|
// 用同一个exam_id获取上一轮的错题(传入round参数只获取上一轮的错题)
|
||||||
console.log(`📋 获取第${currentRound.value - 1}轮错题记录 - exam_id: ${currentExamId.value}`)
|
const previousRound = currentRound.value - 1
|
||||||
|
console.log(`📋 获取第${previousRound}轮错题记录 - exam_id: ${currentExamId.value}, round: ${previousRound}`)
|
||||||
|
|
||||||
const mistakesRes: any = await getMistakes(currentExamId.value)
|
const mistakesRes: any = await getMistakes(currentExamId.value, previousRound)
|
||||||
|
|
||||||
console.log('getMistakes原始响应:', mistakesRes)
|
console.log('getMistakes原始响应:', mistakesRes)
|
||||||
console.log('getMistakes响应结构:', {
|
console.log('getMistakes响应结构:', {
|
||||||
@@ -785,6 +806,7 @@ const recordWrongAnswer = async () => {
|
|||||||
try {
|
try {
|
||||||
await recordMistake({
|
await recordMistake({
|
||||||
exam_id: currentExamId.value,
|
exam_id: currentExamId.value,
|
||||||
|
round: currentRound.value, // 记录当前轮次
|
||||||
question_id: null, // AI生成的题目没有question_id
|
question_id: null, // AI生成的题目没有question_id
|
||||||
knowledge_point_id: q.knowledge_point_id || null,
|
knowledge_point_id: q.knowledge_point_id || null,
|
||||||
question_content: q.title,
|
question_content: q.title,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<h2 class="course-title">{{ courseInfo.title }}</h2>
|
<h2 class="course-title">{{ courseInfo.title }}</h2>
|
||||||
<span class="powered-by">
|
<span class="powered-by">
|
||||||
<el-icon><Connection /></el-icon>
|
<el-icon><Connection /></el-icon>
|
||||||
由 Dify 对话流驱动
|
智能对话助手
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button link @click="clearChat">
|
<el-button link @click="clearChat">
|
||||||
@@ -512,19 +512,37 @@ const scrollToBottom = async () => {
|
|||||||
const loadCourseInfo = async () => {
|
const loadCourseInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const courseId = parseInt(courseInfo.value.id)
|
const courseId = parseInt(courseInfo.value.id)
|
||||||
if (isNaN(courseId)) {
|
if (isNaN(courseId) || courseId <= 0) {
|
||||||
console.warn('无效的课程ID:', courseInfo.value.id)
|
console.warn('无效的课程ID:', courseInfo.value.id)
|
||||||
courseInfo.value.title = '未知课程'
|
courseInfo.value.title = '未知课程'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res: any = await getCourseDetail(courseId)
|
const res: any = await getCourseDetail(courseId)
|
||||||
// API 返回格式是 { code: 200, data: { name: "...", ... } }
|
console.log('课程详情API响应:', res)
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
courseInfo.value.title = res.data.title || res.data.name || '未命名课程'
|
// 兼容不同的响应格式
|
||||||
|
// 格式1: { code: 200, data: { name: "...", ... } }
|
||||||
|
// 格式2: 直接返回课程对象 { name: "...", ... }
|
||||||
|
let courseData = res
|
||||||
|
if (res.code !== undefined && res.data) {
|
||||||
|
// 标准响应格式
|
||||||
|
if (res.code === 200) {
|
||||||
|
courseData = res.data
|
||||||
|
} else {
|
||||||
|
console.warn('获取课程详情失败:', res.message || '未知错误')
|
||||||
|
courseInfo.value.title = '未知课程'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从课程数据中提取名称
|
||||||
|
const courseName = courseData?.title || courseData?.name
|
||||||
|
if (courseName) {
|
||||||
|
courseInfo.value.title = courseName
|
||||||
console.log('课程详情已加载:', courseInfo.value.title)
|
console.log('课程详情已加载:', courseInfo.value.title)
|
||||||
} else {
|
} else {
|
||||||
console.warn('获取课程详情失败:', res.message || '未知错误')
|
console.warn('课程数据中缺少名称字段:', courseData)
|
||||||
courseInfo.value.title = '未知课程'
|
courseInfo.value.title = '未命名课程'
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载课程详情失败:', error)
|
console.error('加载课程详情失败:', error)
|
||||||
|
|||||||
@@ -321,8 +321,7 @@ const pdfScaleStyle = computed(() => {
|
|||||||
const scale = 1 / pdfRenderScale.value
|
const scale = 1 / pdfRenderScale.value
|
||||||
return {
|
return {
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${scale})`,
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top center'
|
||||||
width: `${100 * pdfRenderScale.value}%`
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -911,6 +910,7 @@ onUnmounted(() => {
|
|||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid #e4e7ed;
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.page-controls,
|
.page-controls,
|
||||||
.zoom-controls {
|
.zoom-controls {
|
||||||
@@ -933,10 +933,12 @@ onUnmounted(() => {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
// 高清渲染缩放容器
|
// 高清渲染缩放容器
|
||||||
.pdf-scale-wrapper {
|
.pdf-scale-wrapper {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
:deep(.vue-pdf-embed) {
|
:deep(.vue-pdf-embed) {
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="loadingRecommendations"
|
:loading="analyzing"
|
||||||
@click="refreshRecommendations"
|
@click="refreshRecommendations"
|
||||||
class="refresh-btn"
|
class="refresh-btn"
|
||||||
>
|
>
|
||||||
@@ -405,7 +405,6 @@ const abilityFeedback = ref<AbilityFeedback[]>([])
|
|||||||
|
|
||||||
// AI 分析和推荐相关
|
// AI 分析和推荐相关
|
||||||
const analyzing = ref(false)
|
const analyzing = ref(false)
|
||||||
const loadingRecommendations = ref(false)
|
|
||||||
const selectedPriority = ref('all')
|
const selectedPriority = ref('all')
|
||||||
|
|
||||||
// 计算属性:过滤后的课程
|
// 计算属性:过滤后的课程
|
||||||
@@ -723,81 +722,11 @@ const analyzeSmartBadgeData = async () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新AI推荐课程
|
* 刷新AI推荐课程
|
||||||
|
* 重新调用AI分析接口,获取最新的课程推荐
|
||||||
*/
|
*/
|
||||||
const refreshRecommendations = async () => {
|
const refreshRecommendations = async () => {
|
||||||
loadingRecommendations.value = true
|
// 直接调用AI分析智能工牌数据的方法,复用已有逻辑
|
||||||
|
await analyzeSmartBadgeData()
|
||||||
try {
|
|
||||||
// 模拟AI推荐算法
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
||||||
|
|
||||||
// 根据能力评估结果生成推荐
|
|
||||||
const weakPoints = abilityData.value
|
|
||||||
.filter(item => item.value < 85)
|
|
||||||
.sort((a, b) => a.value - b.value)
|
|
||||||
.slice(0, 3)
|
|
||||||
|
|
||||||
// 更新推荐课程(基于薄弱环节)
|
|
||||||
const newRecommendations = []
|
|
||||||
|
|
||||||
if (weakPoints.some(p => p.name === '沟通技巧')) {
|
|
||||||
newRecommendations.push({
|
|
||||||
id: 1,
|
|
||||||
name: '美容咨询与沟通技巧',
|
|
||||||
description: '提升与客户的沟通能力,学习专业的美容咨询技巧',
|
|
||||||
duration: 8,
|
|
||||||
difficulty: 'medium',
|
|
||||||
learnerCount: 156,
|
|
||||||
priority: 'high',
|
|
||||||
targetWeakPoints: ['沟通技巧'],
|
|
||||||
expectedImprovement: 12,
|
|
||||||
matchScore: 95,
|
|
||||||
progress: 85,
|
|
||||||
examPassed: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weakPoints.some(p => p.name === '应变能力')) {
|
|
||||||
newRecommendations.push({
|
|
||||||
id: 2,
|
|
||||||
name: '皮肤问题识别与处理',
|
|
||||||
description: '学习识别常见皮肤问题,掌握针对性的护理和治疗方案',
|
|
||||||
duration: 12,
|
|
||||||
difficulty: 'hard',
|
|
||||||
learnerCount: 89,
|
|
||||||
priority: 'medium',
|
|
||||||
targetWeakPoints: ['应变能力'],
|
|
||||||
expectedImprovement: 15,
|
|
||||||
matchScore: 88,
|
|
||||||
progress: 95,
|
|
||||||
examPassed: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weakPoints.some(p => p.name === '安全意识')) {
|
|
||||||
newRecommendations.push({
|
|
||||||
id: 3,
|
|
||||||
name: '轻医美项目与效果管理',
|
|
||||||
description: '深入了解各种轻医美项目,掌握效果评估和管理方法',
|
|
||||||
duration: 16,
|
|
||||||
difficulty: 'hard',
|
|
||||||
learnerCount: 234,
|
|
||||||
priority: 'medium',
|
|
||||||
targetWeakPoints: ['安全意识'],
|
|
||||||
expectedImprovement: 10,
|
|
||||||
matchScore: 82,
|
|
||||||
progress: 60,
|
|
||||||
examPassed: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
recommendedCourses.value = newRecommendations
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('获取推荐课程失败')
|
|
||||||
} finally {
|
|
||||||
loadingRecommendations.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user