Compare commits

..

10 Commits

Author SHA1 Message Date
lbw
a50c9a2b16 feat(exam): 添加学生考试历史结果查看功能
- 新增接口获取指定学生的历史考试结果列表
- 数据库层新增根据学生ID查询历史考试记录的查询方法
- 服务层新增获取学生历史考试结果列表的实现
- 前端api新增调用学生考试历史接口的方法
- 学生详情页增加考试历史记录图表展示板块
- 新增考试历史折线图组件,展示正确词数和错误词数的时间变化
- 使用echarts实现折线图并支持点击显示详情
- 更新项目依赖,新增echarts库用于图表展示
2025-12-18 11:30:26 +08:00
lbw
eeeb48d048 feat(student): 添加学生详情页及相关路由和跳转按钮
- 在 class.vue 中增加“详情”按钮,可跳转至对应学生详情页
- 使用 vue-router 的 useRouter 实现页面跳转功能
- 添加 /student/:id 路由,绑定学生详情组件 student.vue
- 新增 student.vue 组件,展示学生详细信息
- 精简 Header.vue, 移除多余导航链接,优化界面展示
2025-12-17 17:34:41 +08:00
lbw
26674ab8a9 feat(student-plan): 添加完成学案功能
- 新增 FinishStudentPlanReqVO 类用于请求参数封装
- 学生端学习计划页面新增“完成”按钮及其交互状态
- 实现 finishLessonPlan API 调用,用于标记学案完成
- 后端新增 finishStudentPlan 接口,处理学案完成逻辑
- StudentLessonPlansDOMapper 增加 finfishStudentPlan 方法及对应 SQL 更新语句
- StudentLessonPlansService 添加 finishStudentLessonPlan 接口实现统计记忆单词数并更新学案状态
- VocabularyBankDOMapper 和 WordMasteryLogDOMapper 增加相关统计查询方法和 SQL
- 前端完善完成按钮状态和操作反馈,防止重复提交
2025-12-17 17:17:49 +08:00
lbw
fd828442b1 feat(plan): 支持学案文件下载功能
- 新增 DownLoadLessonPlanReqVO 请求类用于下载请求封装
- 在前端学案列表增加“下载”按钮,支持单条学案下载操作
- 实现前端下载接口,处理后端返回的 Blob 文件流并触发文件保存
- 后端新增下载接口,根据学案 ID 生成对应的 Word 文档并作为附件响应
- WordExportUtil 中新增按模板生成学案 Word 文档方法,支持工作日和周末模板切换
- LessonPlansService 新增根据 ID 查询学案的方法及对应 Mapper 实现
- 修改学案列表中“学案ID”标签为“计划ID”,提升表述准确性
- 下载过程中添加加载状态和错误信息提示,提升用户体验
2025-12-17 15:56:55 +08:00
lbw
dbe7312633 feat(student-plan): 实现学生学案查询功能
- 新增FindStudentPlansReqVO和FindStudentPlansRspVO定义请求和响应数据结构
- 新增LessonPlanItem用于描述单个学案项
- StudentLessonPlansDO模型新增isFinished属性
- 扩展StudentLessonPlansDOMapper,添加分页及按姓名查询学生学案列表方法及统计总数方法
- 扩展LessonPlansDOMapper,新增按学案ID列表批量查询方法
- 实现StudentLessonPlansService及LessonPlansService接口对应查询方法
- 新增StudentLessonPlansController,提供学生学案分页查询接口
- 在前端LearningPlan.vue添加学案查询界面及分页、搜索功能
- 封装studentLessonPlans接口axios方法,支持分页按姓名查询学生学案数据
- 添加单元测试更新验证数据库查询正确性
2025-12-17 15:29:36 +08:00
lbw
49cd146bc3 feat(plan): 添加生成学案功能及界面支持
- 新增 AddLessonPlanReqVO 数据模型用于生成学案请求参数封装
- 新增 LessonPlanController 提供生成学案的后端接口,支持异步任务执行
- 新增 LessonPlanDialog 组件,实现前端学案生成弹窗及交互逻辑
- 在班级页面添加生成学案按钮,支持单个学生选择后调用弹窗
- 添加 plan.js 接口调用封装,调用后端生成学案接口
- 完成前后端联动,实现生成学案操作的完整流程和提示信息
2025-12-17 11:50:23 +08:00
lbw
07b9b56e8a feat(unit): 新增单元管理功能及相关接口
- 新增单元的请求和响应VO类,实现分页查询单元列表
- 新增UnitController,提供单元列表查询、添加和删除API接口
- 实现UnitService及其实现类,完成单元相关数据库操作和业务逻辑
- 扩展UnitDOMapper及对应XML,实现单元列表和数量查询功能
- 扩展GradeUnitDOMapper,支持单元与年级关联的插入与删除
- 在enlish-vue中新增单元列表展示、分页、删除及新增对话框功能
- 编写AddUnitDialog组件,实现新增单元UI及逻辑
- 新增unit.js接口封装单元相关的API请求
- 注释掉LessonPlansServiceImpl中的导出Word文档相关代码逻辑调整
- 调整class.vue页面样式和布局,集成单元管理模块并优化查询交互
2025-12-17 11:20:04 +08:00
lbw
7f41036193 feat(lessonplan): 实现基于AI的学案自动生成与管理功能
- 新增DifyArticleClient工具类,实现基于Dify API的对话与文本生成功能
- 创建LessonPlansService接口及其实现,实现学案按天生成及存储
- 设计LessonPlansDO和StudentLessonPlansDO数据对象及对应MyBatis映射和数据库操作
- 扩展VocabularyBankDO实体及Mapper,支持查询单元词汇和学生未掌握词汇
- 利用deepoove-poi模板技术生成Word格式的学习计划文档,包含词汇、复习和练习
- 开发StringToPlanMapUtil工具类,解析AI返回结果为结构化学案内容
- 新增JUnit测试用例验证AI对话功能及学案生成逻辑正确性
- 更新Spring Boot配置,添加AI接口地址及密钥等参数
- 在前端Vue项目中新建学案页面,路由配置及导航菜单支持学案访问
2025-12-16 19:08:58 +08:00
lbw
d027c9c7e6 fix(examWords): 处理生成单词为空的异常情况
- 增加判断如果生成的单词对象为空或单词列表为空,则抛出运行时异常
- 防止后续处理中因单词缺失导致的错误
- 提升系统稳定性与异常提示的准确性
2025-12-15 17:25:00 +08:00
lbw
e5fbb445cf feat(class): 删除班级时验证是否存在学生
- 新增StudentDOMapper接口方法selectStudentCountByClassId,用于查询班级下学生数量
- 在ClassServiceImpl中注入StudentDOMapper
- 删除班级时先判断班级下是否存在学生,若存在则抛出异常防止删除
- 更新StudentDOMapper.xml,添加对应的SQL查询语句selectStudentCountByClassId
2025-12-15 16:37:42 +08:00
69 changed files with 2451 additions and 21 deletions

View File

@@ -0,0 +1,10 @@
package com.yinlihupo.enlish.service.constant;
public interface LessonPlanConstant {
String TITLE = "Title";
String PASSAGE = "The Passage";
String QUIZ = "Quiz";
String ANSWER_KEY_EXPLANATION = "Answer Key & Explanation";
String FULL_TRANSLATION = "Full Translation";
}

View File

@@ -50,6 +50,9 @@ public class ExamWordsController {
}
try {
ExamWordsDO examWordsDO = examWordsService.generateExamWords(gradeId, level, studentIds);
if (examWordsDO == null || examWordsDO.getWordIds().isEmpty()) {
throw new RuntimeException("没有单词");
}
List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()
.id(vocabularyBankDO.getId())
@@ -138,4 +141,22 @@ public class ExamWordsController {
return Response.success(examWordsDetailResultRspVO);
}
@PostMapping("student/history")
@ApiOperationLog(description = "获取学生历史考试结果")
Response<List<FindStudentExamWordsResultListRspVO>> getStudentExamWordsResultList(@RequestBody FindStudentExamWordsResultReqVO findStudentExamWordsResultReqVO) {
Integer studentId = findStudentExamWordsResultReqVO.getStudentId();
List<FindStudentExamWordsResultListRspVO> list = examWordsJudgeService.getStudentExamWordsResultList(studentId).stream().map(examWordsJudgeResultDO -> FindStudentExamWordsResultListRspVO.builder()
.id(examWordsJudgeResultDO.getId())
.studentId(examWordsJudgeResultDO.getStudentId())
.examWordsId(examWordsJudgeResultDO.getExamWordsId())
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.startDate(examWordsJudgeResultDO.getStartDate())
.accuracy((double)examWordsJudgeResultDO.getCorrectWordCount() / (examWordsJudgeResultDO.getCorrectWordCount() + examWordsJudgeResultDO.getWrongWordCount()))
.build()
).toList();
return Response.success(list);
}
}

View File

@@ -0,0 +1,71 @@
package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import com.yinlihupo.enlish.service.model.vo.plan.AddLessonPlanReqVO;
import com.yinlihupo.enlish.service.model.vo.plan.DownLoadLessonPlanReqVO;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.utils.WordExportUtil;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.Response;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.Executor;
@RequestMapping("/plan/")
@RestController
@Slf4j
public class LessonPlanController {
@Resource
private LessonPlansService lessonPlanService;
@Resource(name = "taskExecutor")
private Executor taskExecutor;
@Value("${templates.plan.weekday}")
private String planWeekday;
@Value("${templates.plan.weekend}")
private String planWeekend;
@PostMapping("generate")
@ApiOperationLog(description = "生成学案")
public Response<String> generateLessonPlan(@RequestBody AddLessonPlanReqVO addLessonPlanReqVO) {
Integer studentId = addLessonPlanReqVO.getStudentId();
Integer unitId = addLessonPlanReqVO.getUnitId();
try {
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId));
return Response.success("生成学案成功,请等待 10 分钟");
} catch (Exception e) {
log.error(e.getMessage());
return Response.fail("生成学案失败" + e.getMessage());
}
}
@PostMapping("download")
public void downloadLessonPlan(@RequestBody DownLoadLessonPlanReqVO downLoadLessonPlanReqVO, HttpServletResponse response) {
Integer id = downLoadLessonPlanReqVO.getId();
LessonPlansDO lessonPlanById = lessonPlanService.findLessonPlanById(id);
try {
Map<String, Object> map = JsonUtils.parseMap(lessonPlanById.getContentDetails(), String.class, Object.class);
if (!lessonPlanById.getTitle().contains("复习")) {
WordExportUtil.generateLessonPlanDocx(map, lessonPlanById.getTitle(), response, planWeekday, true);
} else {
WordExportUtil.generateLessonPlanDocx(map, lessonPlanById.getTitle(), response, planWeekend, false);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,92 @@
package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO;
import com.yinlihupo.enlish.service.model.bo.StudentDetail;
import com.yinlihupo.enlish.service.model.vo.plan.FindStudentPlansReqVO;
import com.yinlihupo.enlish.service.model.vo.plan.FindStudentPlansRspVO;
import com.yinlihupo.enlish.service.model.vo.plan.FinishStudentPlanReqVO;
import com.yinlihupo.enlish.service.model.vo.plan.LessonPlanItem;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.service.StudentLessonPlansService;
import com.yinlihupo.enlish.service.service.StudentService;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.PageResponse;
import com.yinlihupo.framework.common.response.Response;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RequestMapping("/studentLessonPlans/")
@RestController
public class StudentLessonPlansController {
@Resource
private StudentLessonPlansService studentLessonPlansService;
@Resource
private StudentService studentService;
@Resource
private LessonPlansService lessonPlansService;
@PostMapping("/list")
@ApiOperationLog(description = "查询学生学案")
public PageResponse<FindStudentPlansRspVO> findStudentPlans(@RequestBody FindStudentPlansReqVO findStudentPlansReqVO) {
Integer studentLessonPlanTotal = studentLessonPlansService.findStudentLessonPlanTotal();
String name = findStudentPlansReqVO.getName();
Integer page = findStudentPlansReqVO.getPage();
Integer size = findStudentPlansReqVO.getSize();
List<StudentLessonPlansDO> studentLessonPlansDOListPageSize = studentLessonPlansService.findStudentLessonPlansDOList(page, size, name);
Map<Integer, List<StudentLessonPlansDO>> studentId2StudentLessonPlansDOListMap = studentLessonPlansDOListPageSize.stream().collect(
Collectors.groupingBy(StudentLessonPlansDO::getStudentId)
);
List<Integer> planIds = studentLessonPlansDOListPageSize.stream().map(StudentLessonPlansDO::getPlanId).toList();
List<LessonPlansDO> lessonPlans = lessonPlansService.findLessonPlans(planIds);
Map<Integer, LessonPlansDO> id2LessonPlansDO = lessonPlans.stream().collect(Collectors.toMap(
LessonPlansDO::getId,
lessonPlansDO -> lessonPlansDO
));
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(new ArrayList<>(studentId2StudentLessonPlansDOListMap.keySet()));
List<FindStudentPlansRspVO> findStudentPlansRspVOList = studentDetailList.stream().map(studentDetail -> {
List<StudentLessonPlansDO> studentLessonPlansDOList = studentId2StudentLessonPlansDOListMap.get(studentDetail.getId());
return FindStudentPlansRspVO.builder()
.name(studentDetail.getName())
.id(studentDetail.getId())
.classId(studentDetail.getClassId())
.gradeId(studentDetail.getGradeId())
.gradeName(studentDetail.getGradeName())
.className(studentDetail.getClassName())
.plans(studentLessonPlansDOList.stream().map(studentLessonPlansDO ->
LessonPlanItem.builder().title(id2LessonPlansDO.get(studentLessonPlansDO.getPlanId()).getTitle()).id(studentLessonPlansDO.getPlanId()).isFinished(studentLessonPlansDO.getIsFinished()).build()
).toList())
.build();
}).toList();
return PageResponse.success(findStudentPlansRspVOList, page, studentLessonPlanTotal, size);
}
@PostMapping("/finish")
@ApiOperationLog(description = "完成学案")
public Response<String> finishStudentPlan(@RequestBody FinishStudentPlanReqVO finishStudentPlanReqVO) {
Integer studentId = finishStudentPlanReqVO.getStudentId();
Integer planId = finishStudentPlanReqVO.getPlanId();
int lessonPlan = studentLessonPlansService.finishStudentLessonPlan(studentId, planId);
if (lessonPlan > 0) {
return Response.success("完成学案成功");
}
return Response.fail("完成学案失败");
}
}

View File

@@ -0,0 +1,73 @@
package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.domain.dataobject.UnitDO;
import com.yinlihupo.enlish.service.model.vo.unit.AddUnitReqVO;
import com.yinlihupo.enlish.service.model.vo.unit.DeleteUnitReqVO;
import com.yinlihupo.enlish.service.model.vo.unit.FindUnitListReqVO;
import com.yinlihupo.enlish.service.model.vo.unit.FindUnitListRspVO;
import com.yinlihupo.enlish.service.service.UnitService;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.PageResponse;
import com.yinlihupo.framework.common.response.Response;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequestMapping("/unit/")
@RestController
@Slf4j
public class UnitController {
@Resource
private UnitService unitService;
@PostMapping("/list")
@ApiOperationLog(description = "查询单元")
public PageResponse<FindUnitListRspVO> list(@RequestBody FindUnitListReqVO findUnitListReqVO) {
Integer page = findUnitListReqVO.getPage();
Integer size = findUnitListReqVO.getSize();
Integer unitDOListCount = unitService.findUnitDOListCount();
List<UnitDO> unitDOList = unitService.findUnitDOList(page, size);
List<FindUnitListRspVO> findUnitListRspVOS = unitDOList.stream().map(unitDO -> FindUnitListRspVO.builder()
.id(unitDO.getId())
.title(unitDO.getTitle())
.version(unitDO.getVersion())
.createAt(unitDO.getCreateAt())
.build()
).toList();
return PageResponse.success(findUnitListRspVOS, page, unitDOListCount, size);
}
@PostMapping("/add")
@ApiOperationLog(description = "添加单元")
public Response<Void> add(@RequestBody AddUnitReqVO addUnitReqVO) {
try {
unitService.add(addUnitReqVO);
return Response.success();
} catch (Exception e) {
log.error(e.getMessage());
return Response.fail(e.getMessage());
}
}
@PostMapping("/delete")
@ApiOperationLog(description = "删除单元")
public Response<String> delete(@RequestBody DeleteUnitReqVO deleteUnitReqVO) {
try {
unitService.delete(deleteUnitReqVO.getId());
return Response.success();
} catch (Exception e) {
log.error(e.getMessage());
return Response.fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,27 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class LessonPlansDO {
private Integer id;
private String title;
private String gradeId;
private Integer unitId;
private Date createdAt;
private String contentDetails;
}

View File

@@ -0,0 +1,34 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class StudentLessonPlansDO {
private Integer id;
private Integer studentId;
private Integer planId;
private Date startTime;
private BigDecimal masteryRat;
private Integer totalCount;
private Integer isFinished;
private String memorizedWordsJson;
private String unmemorizedWordsJson;
}

View File

@@ -10,6 +10,7 @@ import lombok.NoArgsConstructor;
@Data
@Builder
public class VocabularyBankDO {
private Integer id;
private String word;
@@ -18,6 +19,8 @@ public class VocabularyBankDO {
private String pronunciation;
private String pos;
private Integer unitId;
}

View File

@@ -20,4 +20,6 @@ public interface ExamWordsJudgeResultDOMapper {
Integer selectCount();
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
}

View File

@@ -1,5 +1,6 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.GradeUnitDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -7,4 +8,10 @@ import java.util.List;
public interface GradeUnitDOMapper {
List<Integer> selectUnitIdsByGradeId(@Param("gradeId") Integer gradeId);
GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId);
int insert(GradeUnitDO record);
int deleteByUnitId(@Param("unitId") Integer unitId);
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface LessonPlansDOMapper {
void insert(LessonPlansDO lessonPlansDO);
LessonPlansDO selectById(Integer id);
List<LessonPlansDO> findLessonPlansByStudentId(@Param("ids") List<Integer> ids);
LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId);
}

View File

@@ -19,4 +19,6 @@ public interface StudentDOMapper {
// 逻辑删除
void deleteById(Integer id);
int selectStudentCountByClassId(@Param("classId") Integer classId);
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface StudentLessonPlansDOMapper {
void insert(StudentLessonPlansDO studentLessonPlansDO);
List<StudentLessonPlansDO> selectStudentLessonPlanList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize, @Param("name") String name);
Integer selectStudentPlanTotal();
Integer finfishStudentPlan(@Param("studentId") Integer studentId, @Param("planId") Integer planId, @Param("count") Integer count, @Param("mastery") double mastery);
}

View File

@@ -1,6 +1,9 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.UnitDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UnitDOMapper {
int deleteByPrimaryKey(Integer id);
@@ -18,4 +21,8 @@ public interface UnitDOMapper {
int updateByPrimaryKey(UnitDO record);
UnitDO selectByTitle(String title);
List<UnitDO> selectUnitDOList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
Integer selectUnitDOListCount();
}

View File

@@ -14,4 +14,12 @@ public interface VocabularyBankDOMapper {
List<VocabularyBankDO> selectVocabularyBankDOListByIds(@Param("ids") List<Integer> ids);
List<Integer> selectAllIds();
List<VocabularyBankDO> selectVocabularyBankDOAllByUnitId(@Param("unitId") Integer unitId);
List<VocabularyBankDO> selectVocabularyBankListStudentNotMaster(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId);
List<VocabularyBankDO> selectVocabularyBankListSelfCheck(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
Integer selectWordTotal();
}

View File

@@ -14,4 +14,6 @@ public interface WordMasteryLogDOMapper {
int batchInsertInitialization(@Param("wordIds") List<Integer> wordIds, @Param("studentId") Integer studentId);
int batchUpdateStudentMastery(@Param("wordMasteryLogDOs") List<WordMasteryLogDO> wordMasteryLogDOs);
int selectStudentStrengthCount(@Param("studentId") Integer studentId);
}

View File

@@ -0,0 +1,29 @@
package com.yinlihupo.enlish.service.model.vo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentExamWordsResultListRspVO {
private Integer id;
private Integer studentId;
private Integer examWordsId;
private Integer correctWordCount;
private Integer wrongWordCount;
private Double accuracy;
private LocalDateTime startDate;
}

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentExamWordsResultReqVO {
private Integer studentId;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class AddLessonPlanReqVO {
private Integer studentId;
private Integer unitId;
}

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class DownLoadLessonPlanReqVO {
private Integer id;
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentPlansReqVO {
String name;
Integer page;
Integer size;
}

View File

@@ -0,0 +1,24 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentPlansRspVO {
private Integer id;
private String name;
private Integer classId;
private String className;
private Integer gradeId;
private String gradeName;
List<LessonPlanItem> plans;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class FinishStudentPlanReqVO {
private Integer studentId;
private Integer planId;
}

View File

@@ -0,0 +1,19 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class LessonPlanItem {
private Integer id;
private String title;
private Integer isFinished;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.unit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class AddUnitReqVO {
private String title;
private Integer gradeId;
}

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.unit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class DeleteUnitReqVO {
private Integer id;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.unit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindUnitListReqVO {
private Integer page;
private Integer size;
}

View File

@@ -0,0 +1,35 @@
package com.yinlihupo.enlish.service.model.vo.unit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindUnitListRspVO {
/**
* 主键
*/
private Integer id;
/**
* 年级/单元
*/
private String title;
/**
* 版本
*/
private String version;
/**
* 创建时间
*/
private LocalDateTime createAt;
}

View File

@@ -2,6 +2,7 @@ package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO;
import java.util.Arrays;
import java.util.List;
public interface ExamWordsJudgeService {
@@ -13,4 +14,6 @@ public interface ExamWordsJudgeService {
Integer getExamWordsJudgeResultCount();
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);
}

View File

@@ -0,0 +1,13 @@
package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import java.util.List;
public interface LessonPlansService {
void generateLessonPlans(Integer studentId, Integer unitId);
List<LessonPlansDO> findLessonPlans(List<Integer> ids);
LessonPlansDO findLessonPlanById(Integer id);
}

View File

@@ -0,0 +1,14 @@
package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO;
import java.util.List;
public interface StudentLessonPlansService {
List<StudentLessonPlansDO> findStudentLessonPlansDOList(Integer page, Integer size, String name);
Integer findStudentLessonPlanTotal();
int finishStudentLessonPlan(Integer studentId, Integer planId);
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.UnitDO;
import com.yinlihupo.enlish.service.model.vo.unit.AddUnitReqVO;
import java.util.List;
public interface UnitService {
List<UnitDO> findUnitDOList(Integer page, Integer size);
Integer findUnitDOListCount();
void add(AddUnitReqVO addUnitReqVO);
void delete(Integer id);
}

View File

@@ -6,6 +6,7 @@ import com.yinlihupo.enlish.service.domain.dataobject.GradeDO;
import com.yinlihupo.enlish.service.domain.mapper.ClassDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.GradeClassDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.GradeDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.StudentDOMapper;
import com.yinlihupo.enlish.service.service.ClassService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -24,6 +25,8 @@ public class ClassServiceImpl implements ClassService {
private GradeClassDOMapper gradeClassDOMapper;
@Resource
private GradeDOMapper gradeDOMapper;
@Resource
private StudentDOMapper studentDOMapper;
@Override
public ClassDO findClassById(Integer id) {
@@ -82,6 +85,10 @@ public class ClassServiceImpl implements ClassService {
@Override
public void deleteClass(Integer classId) {
int selectStudentCountByClassId = studentDOMapper.selectStudentCountByClassId(classId);
if (selectStudentCountByClassId > 0) {
throw new RuntimeException("该班级下有学生,请先删除该班级下的学生");
}
classDOMapper.delete(classId);
}
}

View File

@@ -144,4 +144,9 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
return examWordsJudgeResultDOMapper.selectDetailById(id);
}
@Override
public List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId) {
return examWordsJudgeResultDOMapper.selectByStudentId(studentId);
}
}

View File

@@ -0,0 +1,300 @@
package com.yinlihupo.enlish.service.service.plan;
import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.*;
@Service
@Slf4j
public class LessonPlansServiceImpl implements LessonPlansService {
@Resource
private LessonPlansDOMapper lessonPlansDOMapper;
@Resource
private StudentLessonPlansDOMapper studentLessonPlansDOMapper;
@Resource
private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
private UnitDOMapper unitDOMapper;
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource
private GradeDOMapper gradeDOMapper;
@Resource
private DifyArticleClient difyArticleClient;
@Value("${templates.plan.weekday}")
private String planWeekday;
@Value("${templates.plan.weekend}")
private String planWeekend;
@Override
@Transactional(rollbackFor = Exception.class)
public void generateLessonPlans(Integer studentId, Integer unitId) {
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId());
// 补差词汇所用词汇的
List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper
.selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50);
int gapSize = vocabularyBankListStudentNotMaster.size();
int countGap = gapSize / 5;
int syncSize = vocabularyBankDOS.size();
int countSync = syncSize / 5;
int checkTotal = 50;
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
for (int i = 0; i < 5; i++) {
List<VocabularyBankDO> syncVocabList = vocabularyBankDOS.subList(i * countSync, Math.min((i + 1) * countSync, syncSize));
List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize));
weeksSync.add(syncVocabList);
weeksGap.add(gapVocabList);
List<VocabularyBankDO> reviewVocabList = new ArrayList<>();
List<VocabularyBankDO> checkList = new ArrayList<>();
// 艾宾浩斯遗忘曲线
switch (i) {
case 1 -> reviewVocabList.addAll(syncVocabList);
case 2 -> {
reviewVocabList.addAll(weeksSync.get(0));
reviewVocabList.addAll(weeksSync.get(1));
checkList.addAll(weeksSync.get(1));
checkList.addAll(weeksSync.get(1));
}
case 3 -> {
reviewVocabList.addAll(weeksSync.get(1));
reviewVocabList.addAll(weeksGap.get(2));
checkList.addAll(weeksSync.get(1));
checkList.addAll(weeksSync.get(2));
}
case 4 -> {
reviewVocabList.addAll(weeksSync.get(2));
reviewVocabList.addAll(weeksGap.get(3));
checkList.addAll(weeksSync.get(2));
checkList.addAll(weeksSync.get(3));
}
}
List<VocabularyBankDO> checkWords = vocabularyBankDOMapper
.selectVocabularyBankListSelfCheck(gradeUnitDO.getGradeId(), studentId, checkList.stream().map(VocabularyBankDO::getId).toList(), Math.max(checkTotal - checkList.size(), 0));
checkList.addAll(checkWords);
Map<String, Object> lessonPlanMap = null;
try {
lessonPlanMap = generateWeekdayPlans(syncVocabList, gapVocabList, reviewVocabList, checkList, i + 1, gradeDO, unitDO, studentId);
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
.title(lessonPlanMap.get("title").toString())
.gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId())
.createdAt(new Date())
.contentDetails(JsonUtils.toJsonString(lessonPlanMap))
.build();
lessonPlansDOMapper.insert(lessonPlansDO);
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
.studentId(studentId)
.planId(lessonPlansDO.getId())
.build();
studentLessonPlansDOMapper.insert(studentLessonPlansDO);
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("生成第{}天计划成功", i + 1);
}
try {
int syncWeekender = syncSize / 2;
for (int i = 0; i < 2; i++) {
List<VocabularyBankDO> checkList = vocabularyBankDOS.subList(i * syncWeekender, Math.min((i + 1) * syncWeekender, syncSize));
Map<String, Object> map = generateWeekendPlans(checkList, i + 6, gradeDO, unitDO, studentId);
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
.title(map.get("title").toString())
.gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId())
.createdAt(new Date())
.contentDetails(JsonUtils.toJsonString(map))
.build();
lessonPlansDOMapper.insert(lessonPlansDO);
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
.studentId(studentId)
.planId(lessonPlansDO.getId())
.build();
studentLessonPlansDOMapper.insert(studentLessonPlansDO);
log.info("生成第{}天计划成功", i + 6);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public List<LessonPlansDO> findLessonPlans(List<Integer> ids) {
return lessonPlansDOMapper.findLessonPlansByStudentId(ids);
}
@Override
public LessonPlansDO findLessonPlanById(Integer id) {
return lessonPlansDOMapper.selectByLessonId(id);
}
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> checkList,
int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException {
Map<String, Object> data = new HashMap<>();
data.put("title", "" + day + "" + "复习" + gradeDO.getTitle() + unitDO.getTitle() + studentId);
data.put("checkList", checkList);
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// Configure config = Configure.builder()
// .bind("checkList", policy)
// .build();
//
// XWPFTemplate template = XWPFTemplate.compile(planWeekend, config);
// template.render(data);
// template.writeAndClose(new FileOutputStream("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\tmp\\word" + "复习" + day + ".docx"));
return data;
}
private Map<String, Object> generateWeekdayPlans(List<VocabularyBankDO> syncVocabList,
List<VocabularyBankDO> gapVocabList,
List<VocabularyBankDO> reviewVocabList,
List<VocabularyBankDO> checkList,
int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + "" + studentId;
Map<String, Object> data = new HashMap<>();
data.put("title", title);
data.put("syncVocabList", syncVocabList);
data.put("gapVocabList", gapVocabList);
data.put("reviewVocabList", reviewVocabList);
data.put("checkList", checkList);
data.put("checkListAns", checkList);
// 中译英
List<VocabularyBankDO> drillRound1 = new ArrayList<>(syncVocabList);
Collections.shuffle(drillRound1);
data.put("drillRound1", drillRound1);
List<VocabularyBankDO> drillRound2 = new ArrayList<>(syncVocabList);
Collections.shuffle(drillRound2);
data.put("drillRound2", drillRound2);
List<VocabularyBankDO> drillRound3 = new ArrayList<>(syncVocabList);
Collections.shuffle(drillRound3);
data.put("drillRound3", drillRound3);
// 英译中
List<VocabularyBankDO> mixedDrill = new ArrayList<>();
mixedDrill.addAll(syncVocabList);
mixedDrill.addAll(gapVocabList);
mixedDrill.addAll(reviewVocabList);
Collections.shuffle(mixedDrill);
data.put("mixedDrill", mixedDrill);
// 文章 A
log.info("生成文章 A 中文开始");
Map<String, String> mapA = getArticleStringMap(gradeDO, unitDO, studentId, syncVocabList);
log.info("生成文章 A 成功 {}", mapA);
data.put("articleATitle", mapA.get(LessonPlanConstant.TITLE));
data.put("articleApassage", mapA.get(LessonPlanConstant.PASSAGE));
data.put("articleAquiz", mapA.get(LessonPlanConstant.QUIZ));
data.put("articleAans", mapA.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
data.put("articleAtran", mapA.get(LessonPlanConstant.FULL_TRANSLATION));
// 文章 B
log.info("生成文章 B 中文开始");
Map<String, String> mapB;
List<VocabularyBankDO> wordsArticleB = new ArrayList<>();
wordsArticleB.addAll(syncVocabList);
wordsArticleB.addAll(gapVocabList);
wordsArticleB.addAll(reviewVocabList);
mapB = getArticleStringMap(gradeDO, unitDO, studentId, wordsArticleB);
log.info("生成文章 B 成功 {}", mapB);
data.put("articleBTitle", mapB.get(LessonPlanConstant.TITLE));
data.put("articleBpassage", mapB.get(LessonPlanConstant.PASSAGE));
data.put("articleBquiz", mapB.get(LessonPlanConstant.QUIZ));
data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
data.put("articleBtran", mapB.get(LessonPlanConstant.FULL_TRANSLATION));
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// Configure config = Configure.builder()
// .bind("syncVocabList", policy)
// .bind("gapVocabList", policy)
// .bind("reviewVocabList", policy)
// .bind("drillRound1", policy)
// .bind("drillRound2", policy)
// .bind("drillRound3", policy)
// .bind("mixedDrill", policy)
// .bind("checkList", policy)
// .bind("checkListAns", policy)
// .build();
//
// XWPFTemplate template = XWPFTemplate.compile(planWeekday, config);
// template.render(data);
// template.writeAndClose(new FileOutputStream("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\tmp\\word" + title + ".docx"));
return data;
}
@NonNull
private Map<String, String> getArticleStringMap(GradeDO gradeDO, UnitDO unitDO, Integer studentId, List<VocabularyBankDO> words) throws Exception {
Map<String, String> map;
StringBuilder sb = new StringBuilder();
words.forEach(word -> sb.append(word.getWord()).append(","));
sb.deleteCharAt(sb.length() - 1);
String string = sb.toString();
int i = 0;
do {
log.info("第{}次生成文章中文开始", ++i);
String answer = difyArticleClient.sendChat(string, String.valueOf(studentId) + UUID.randomUUID(), null).getAnswer();
map = StringToPlanMapUtil.parseTextToMap(answer);
} while (map.get(LessonPlanConstant.TITLE) == null
|| map.get(LessonPlanConstant.PASSAGE) == null
|| map.get(LessonPlanConstant.QUIZ) == null
|| map.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION) == null
|| map.get(LessonPlanConstant.FULL_TRANSLATION) == null
);
return map;
}
public List<VocabularyBankDO> getVocabListRandom(List<VocabularyBankDO> list, int count) {
List<VocabularyBankDO> randomResultList;
int listSize = list.size();
if (listSize <= count) {
randomResultList = new ArrayList<>(list);
} else {
List<VocabularyBankDO> tempList = new ArrayList<>(list);
Collections.shuffle(tempList); // 随机打乱列表顺序
randomResultList = new ArrayList<>(tempList.subList(0, count));
}
return randomResultList;
}
}

View File

@@ -0,0 +1,47 @@
package com.yinlihupo.enlish.service.service.plan;
import com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO;
import com.yinlihupo.enlish.service.domain.mapper.StudentLessonPlansDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper;
import com.yinlihupo.enlish.service.service.StudentLessonPlansService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class StudentLessonPlansServiceImpl implements StudentLessonPlansService {
@Resource
private StudentLessonPlansDOMapper studentLessonPlansDOMapper;
@Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Resource
private VocabularyBankDOMapper vocabularyBankDOMapper;
@Override
public List<StudentLessonPlansDO> findStudentLessonPlansDOList(Integer page, Integer size, String name) {
log.info("查询学生学案");
if (name.isEmpty()) {
return studentLessonPlansDOMapper.selectStudentLessonPlanList((page - 1) * size, size, null);
}
return studentLessonPlansDOMapper.selectStudentLessonPlanList((page - 1) * size, size, name);
}
@Override
public Integer findStudentLessonPlanTotal() {
return studentLessonPlansDOMapper.selectStudentPlanTotal();
}
@Override
public int finishStudentLessonPlan(Integer studentId, Integer planId) {
int wordStrengthCount = wordMasteryLogDOMapper.selectStudentStrengthCount(studentId);
Integer wordTotal = vocabularyBankDOMapper.selectWordTotal();
return studentLessonPlansDOMapper.finfishStudentPlan(studentId, planId, wordStrengthCount, (double) wordStrengthCount / wordTotal);
}
}

View File

@@ -0,0 +1,51 @@
package com.yinlihupo.enlish.service.service.unit;
import com.yinlihupo.enlish.service.domain.dataobject.GradeUnitDO;
import com.yinlihupo.enlish.service.domain.dataobject.UnitDO;
import com.yinlihupo.enlish.service.domain.mapper.GradeUnitDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.UnitDOMapper;
import com.yinlihupo.enlish.service.model.vo.unit.AddUnitReqVO;
import com.yinlihupo.enlish.service.service.UnitService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class UnitServiceImpl implements UnitService {
@Resource
private UnitDOMapper unitDOMapper;
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Override
public List<UnitDO> findUnitDOList(Integer page, Integer size) {
return unitDOMapper.selectUnitDOList((page - 1) * size, size);
}
@Override
public Integer findUnitDOListCount() {
return unitDOMapper.selectUnitDOListCount();
}
@Override
public void add(AddUnitReqVO addUnitReqVO) {
UnitDO unitDO = UnitDO.builder()
.title(addUnitReqVO.getTitle())
.createAt(LocalDateTime.now())
.build();
unitDOMapper.insertSelective(unitDO);
Integer gradeId = addUnitReqVO.getGradeId();
gradeUnitDOMapper.insert(GradeUnitDO.builder().gradeId(gradeId).unitId(unitDO.getId()).build());
}
@Override
public void delete(Integer id) {
unitDOMapper.deleteByPrimaryKey(id);
gradeUnitDOMapper.deleteByUnitId(id);
}
}

View File

@@ -0,0 +1,118 @@
package com.yinlihupo.enlish.service.utils; // 修改为你的包名
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Component
public class DifyArticleClient {
@Value("${ai.key}")
private String apiKey;
@Value("${ai.url}")
private String baseUrl;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
// 构造函数
public DifyArticleClient() {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // 连接超时
.build();
this.objectMapper = new ObjectMapper();
}
/**
* 发送对话请求 (阻塞模式)
*
* @param query 用户的问题
* @param userId 用户唯一标识
* @param conversationId 会话ID (首次传 null 或 空字符串)
* @return DifyResponse 包含回复内容和新的 conversationId
*/
public DifyResponse sendChat(String query, String userId, String conversationId) throws Exception {
String endpoint = this.baseUrl;
// 1. 构建请求体对象
ChatRequest payload = new ChatRequest();
payload.setQuery(query);
payload.setUser(userId);
payload.setResponseMode("blocking"); // 使用阻塞模式,一次性返回
// 如果有 conversationId带上它以保持上下文
if (conversationId != null && !conversationId.isEmpty()) {
payload.setConversationId(conversationId);
}
// 如果你的 Dify 应用没有定义变量inputs 传空 Map 即可,但字段必须存在
payload.setInputs(new HashMap<>());
// 2. 序列化为 JSON 字符串
String jsonBody = objectMapper.writeValueAsString(payload);
// 3. 构建 HTTP 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Authorization", "Bearer " + this.apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.timeout(Duration.ofSeconds(30)) // 读取超时
.build();
// 4. 发送请求
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 5. 检查状态码
if (response.statusCode() != 200) {
throw new RuntimeException("Dify 请求失败: HTTP " + response.statusCode() + " | Body: " + response.body());
}
// 6. 反序列化响应体
return objectMapper.readValue(response.body(), DifyResponse.class);
}
// ================= 内部类DTO (数据传输对象) =================
// 请求体结构
@Data
@ToString
public static class ChatRequest {
private Map<String, Object> inputs;
private String query;
@JsonProperty("response_mode")
private String responseMode;
@JsonProperty("conversation_id")
private String conversationId;
private String user;
}
// 响应体结构
// ignoreUnknown = true 非常重要Dify 返回很多元数据,我们只映射需要的字段,防止报错
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public static class DifyResponse {
private String answer; // 核心回复内容
@JsonProperty("conversation_id")
private String conversationId; // 会话 ID
@JsonProperty("message_id")
private String messageId;
private long created_at;
}
}

View File

@@ -0,0 +1,73 @@
package com.yinlihupo.enlish.service.utils;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringToPlanMapUtil {
public static Map<String, String> parseTextToMap(String text) {
// 使用 LinkedHashMap 保持插入顺序
Map<String, String> map = new LinkedHashMap<>();
// 正则表达式:匹配行首的 **Key:** 格式
Pattern headerPattern = Pattern.compile("^\\s*\\*\\*(.+?):\\*\\*\\s*(.*)");
String currentKey = null;
StringBuilder currentValue = new StringBuilder();
try (Scanner scanner = new Scanner(text)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
Matcher matcher = headerPattern.matcher(line);
if (matcher.matches()) {
// 如果发现新的 Header
// 1. 保存上一个 Key-Value 对(如果存在)
if (currentKey != null) {
// 修改处:在存入 Map 前,调用 cleanMdBold 去除粗体语法
map.put(currentKey, cleanMdBold(currentValue.toString()));
}
// 2. 更新当前的 Key
currentKey = matcher.group(1).trim();
// 3. 重置 StringBuilder
currentValue = new StringBuilder();
String remainingText = matcher.group(2);
if (!remainingText.isEmpty()) {
currentValue.append(remainingText).append("\n");
}
} else {
// 如果不是 Header 行,追加到当前 Value 中
if (currentKey != null) {
currentValue.append(line).append("\n");
}
}
}
// 循环结束后,保存最后一个 Key-Value 对
if (currentKey != null) {
// 修改处:同样在最后存入时去除粗体语法
map.put(currentKey, cleanMdBold(currentValue.toString()));
}
}
return map;
}
/**
* 辅助方法:去除字符串两端的空白以及内部的 Markdown 粗体符号 (**)
*/
private static String cleanMdBold(String text) {
if (text == null) {
return "";
}
// 1. 替换掉所有的 ** 符号
// 2. 去除首尾空白
return text.replace("**", "").trim();
}
}

View File

@@ -16,6 +16,8 @@ import java.util.zip.ZipOutputStream;
public class WordExportUtil {
private static final Configure config;
private static final Configure configLessonPlanWeekday;
private static final Configure configLessonPlanWeekend;
static {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
@@ -23,6 +25,24 @@ public class WordExportUtil {
.bind("words", policy)
.bind("answer", policy)
.build();
LoopRowTableRenderPolicy policyLessonPlanWeekday = new LoopRowTableRenderPolicy();
configLessonPlanWeekday = Configure.builder()
.bind("syncVocabList", policyLessonPlanWeekday)
.bind("gapVocabList", policyLessonPlanWeekday)
.bind("reviewVocabList", policyLessonPlanWeekday)
.bind("drillRound1", policyLessonPlanWeekday)
.bind("drillRound2", policyLessonPlanWeekday)
.bind("drillRound3", policyLessonPlanWeekday)
.bind("mixedDrill", policyLessonPlanWeekday)
.bind("checkList", policyLessonPlanWeekday)
.bind("checkListAns", policyLessonPlanWeekday)
.build();
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
configLessonPlanWeekend = Configure.builder()
.bind("checkList", policyLessonPlan)
.build();
}
/**
@@ -47,6 +67,28 @@ public class WordExportUtil {
}
}
public static void generateLessonPlanDocx(Map<String, Object> map, String fileName, HttpServletResponse response, String templateWordPath, boolean isWeekday) throws IOException {
fileName = URLEncoder.encode(fileName + ".docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
// 3. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);
try (InputStream inputStream = new FileInputStream(templateWordPath)) {
XWPFTemplate template;
if (isWeekday) {
template = XWPFTemplate.compile(inputStream, configLessonPlanWeekday);
} else {
template = XWPFTemplate.compile(inputStream, configLessonPlanWeekend);
}
OutputStream out = response.getOutputStream();
template.render(map);
template.write(out);
template.close();
out.flush();
}
}
/**
* 核心补充:批量渲染并打包为 ZIP
*/
@@ -103,7 +145,7 @@ public class WordExportUtil {
*/
private static void generateExamWordsDocx(Map<String, Object> data, HttpServletResponse response, String templateWordPath) throws IOException {
String fileName = URLEncoder.encode("摸底测试" + ".docx", StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
String fileName = URLEncoder.encode("摸底测试" + ".docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
// 3. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");

View File

@@ -25,5 +25,13 @@ templates:
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v5.docx
count: 100
data: C:\project\tess
plan:
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v1.docx
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v1.docx
plan_day: 7
tmp:
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
ai:
key: app-loC6IrJpj4cS54MAYp73QtGl
url: https://chat.cosonggle.com/v1/chat-messages

View File

@@ -45,7 +45,7 @@
targetProject="src/main/java"/>
<!-- 需要生成的表-实体类 -->
<table tableName="grade_class" domainObjectName="GradeClassDO"
<table tableName="student_lesson_plans" domainObjectName="StudentLessonPlansDO"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"

View File

@@ -66,5 +66,12 @@
from exam_words_judge_result
where id = #{id}
</select>
<select id="selectByStudentId" resultMap="BaseResultMap">
select *
from exam_words_judge_result
where student_id = #{studentId}
order by start_date desc
limit 500;
</select>
</mapper>

View File

@@ -13,5 +13,19 @@
where grade_id = #{gradeId}
</select>
<select id="selectByUnitId" resultMap="BaseResultMap">
select *
from grade_unit
where unit_id = #{unitId}
</select>
<insert id="insert">
insert into grade_unit (grade_id, unit_id)
values (#{gradeId}, #{unitId})
</insert>
<delete id="deleteByUnitId">
delete from grade_unit
where unit_id = #{unitId}
</delete>
</mapper>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yinlihupo.enlish.service.domain.mapper.LessonPlansDOMapper">
<resultMap id="BaseResultMap" type="com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="title" jdbcType="VARCHAR" property="title" />
<result column="grade_id" jdbcType="VARCHAR" property="gradeId" />
<result column="unit_id" jdbcType="INTEGER" property="unitId" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO">
<result column="content_details" jdbcType="LONGVARCHAR" property="contentDetails" />
</resultMap>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into lesson_plans (title, grade_id, unit_id, content_details, created_at)
values (#{title}, #{gradeId}, #{unitId}, #{contentDetails}, now())
</insert>
<select id="selectById" resultMap="ResultMapWithBLOBs">
select *
from lesson_plans
where id = #{id}
</select>
<select id="findLessonPlansByStudentId" resultMap="BaseResultMap">
select *
from lesson_plans
<if test="ids != null">
where id in
<foreach item="id" collection="ids" separator="," open="(" close=")" index="">
#{id}
</foreach>
</if>
</select>
<select id="selectByLessonId" resultMap="ResultMapWithBLOBs">
select *
from lesson_plans
where id = #{lessonId}
</select>
</mapper>

View File

@@ -61,4 +61,11 @@
set is_deleted = 1
where id = #{id}
</update>
<select id="selectStudentCountByClassId" resultType="java.lang.Integer">
select count(1)
from student
where class_id = #{classId}
and is_deleted = 0
</select>
</mapper>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yinlihupo.enlish.service.domain.mapper.StudentLessonPlansDOMapper">
<resultMap id="BaseResultMap" type="com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="student_id" jdbcType="INTEGER" property="studentId" />
<result column="plan_id" jdbcType="INTEGER" property="planId" />
<result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
<result column="mastery_rat" jdbcType="DECIMAL" property="masteryRat" />
<result column="total_count" jdbcType="INTEGER" property="totalCount" />
<result column="is_finished" jdbcType="INTEGER" property="isFinished" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO">
<result column="memorized_words_json" jdbcType="LONGVARCHAR" property="memorizedWordsJson" />
<result column="unmemorized_words_json" jdbcType="LONGVARCHAR" property="unmemorizedWordsJson" />
</resultMap>
<insert id="insert">
insert into student_lesson_plans (student_id, plan_id, start_time, mastery_rat, total_count)
values (#{studentId,jdbcType=INTEGER}, #{planId,jdbcType=INTEGER},
now(), 0.0, 0
)
</insert>
<select id="selectStudentLessonPlanList" resultMap="BaseResultMap">
SELECT slp.*
FROM student_lesson_plans slp
LEFT JOIN student s ON slp.student_id = s.id
JOIN (
SELECT DISTINCT slp_in.student_id
FROM student_lesson_plans slp_in
LEFT JOIN student s_in ON slp_in.student_id = s_in.id
<if test="name != null and name != ''">
WHERE s_in.name LIKE CONCAT('%', #{name}, '%')
</if>
ORDER BY slp_in.student_id
LIMIT #{startIndex}, #{pageSize}
) AS temp ON slp.student_id = temp.student_id
where slp.is_finished = 0
ORDER BY slp.id
</select>
<select id="selectStudentPlanTotal">
SELECT count(DISTINCT student_id) AS total
FROM student_lesson_plans;
</select>
<update id="finfishStudentPlan">
update student_lesson_plans
set is_finished = 1,
total_count = #{count},
mastery_rat = #{mastery}
where student_id = #{studentId}
and plan_id = #{planId}
</update>
</mapper>

View File

@@ -91,4 +91,15 @@
where title = #{title}
</select>
<select id="selectUnitDOList" resultMap="BaseResultMap">
select *
from unit
limit #{startIndex}, #{pageSize}
</select>
<select id="selectUnitDOListCount" resultType="java.lang.Integer">
select count(*)
from unit
</select>
</mapper>

View File

@@ -7,6 +7,7 @@
<result column="definition" jdbcType="VARCHAR" property="definition" />
<result column="pronunciation" jdbcType="VARCHAR" property="pronunciation" />
<result column="unit_id" jdbcType="INTEGER" property="unitId" />
<result column="pos" jdbcType="VARCHAR" property="pos" />
</resultMap>
@@ -28,6 +29,9 @@
<if test="unitId != null">
unit_id,
</if>
<if test="pos != null">
pos,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@@ -45,6 +49,9 @@
<if test="unitId != null">
#{unitId,jdbcType=INTEGER},
</if>
<if test="pos != null">
#{pos,jdbcType=VARCHAR},
</if>
</trim>
</insert>
@@ -69,4 +76,50 @@
from vocabulary_bank
</select>
<select id="selectVocabularyBankDOAllByUnitId" resultMap="BaseResultMap">
select *
from vocabulary_bank
where unit_id = #{unitId}
</select>
<select id="selectVocabularyBankListStudentNotMaster" resultMap="BaseResultMap">
<![CDATA[
select *
from vocabulary_bank
where unit_id in (
select unit_id
from grade_unit
where grade_id = #{gradeId}
)
and id in (
select word_id
from word_mastery_log
where memory_strength < 0
and student_id = #{studentId}
)
]]>
</select>
<select id="selectVocabularyBankListSelfCheck" resultMap="BaseResultMap">
select *
from vocabulary_bank vb
inner join grade_unit gu on vb.unit_id = gu.unit_id
inner join word_mastery_log wml on vb.id = wml.word_id
where gu.grade_id = #{gradeId}
and wml.student_id = #{studentId}
<!-- 修复not in的语法错误 + 空集合防护 -->
<if test="ids != null and ids.size() > 0">
and vb.id not in (
<foreach item="id" collection="ids" separator=",">
#{id}
</foreach>
)
</if>
limit #{wordCount}
</select>
<select id="selectWordTotal" resultType="java.lang.Integer">
select count(*)
from vocabulary_bank
</select>
</mapper>

View File

@@ -22,6 +22,12 @@
order by memory_strength desc
limit #{limit}
</select>
<select id="selectStudentStrengthCount" resultType="java.lang.Integer">
select count(*)
from word_mastery_log
where student_id = #{studentId}
and memory_strength > 0
</select>
<insert id="batchInsertInitialization">
insert into word_mastery_log (student_id, word_id, update_time)

View File

@@ -0,0 +1,50 @@
package com.yinlihupo.enlish.service.ai;
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.Map;
@SpringBootTest
public class AITest {
@Resource
private DifyArticleClient client;
@Test
public void test1() throws IOException {
try {
// 2. 第一轮对话 (没有 conversation_id)
System.out.println("--- Round 1 ---");
String userId = "user-1001";
DifyArticleClient.DifyResponse response1 = client.sendChat("ruler, pencil, eraser, crayon, bag, pen, book, red, green, yellow, blue, face, ear, eye, nose, mouth, duck, pig, cat, bear, dog, elephant, monkey, bird, tiger, panda, bread, juice, egg, milk", userId, null);
//System.out.println("AI 回复: " + response1.getAnswer());
System.out.println("当前会话ID: " + response1.getConversationId());
// // 3. 第二轮对话 (传入上一轮的 conversation_id 以保持记忆)
// System.out.println("\n--- Round 2 ---");
// // 注意这里传入了 response1.getConversationId()
// DifyClient.DifyResponse response2 = client.sendChat("我刚才说了我叫什么?", userId, response1.getConversationId());
//
// System.out.println("AI 回复: " + response2.getAnswer());
System.out.println("\n--- Round 2 ---");
Map<String, String> stringStringMap = StringToPlanMapUtil.parseTextToMap(response1.getAnswer());
System.out.println(stringStringMap.get("Title"));
System.out.println(stringStringMap.get("The Passage"));
System.out.println(stringStringMap.get("Quiz"));
System.out.println(stringStringMap.get("Answer Key & Explanation"));
System.out.println(stringStringMap.get("Full Translation"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,28 @@
package com.yinlihupo.enlish.service.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import com.yinlihupo.enlish.service.domain.mapper.LessonPlansDOMapper;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
@SpringBootTest
public class PlanTest {
@Resource
private LessonPlansDOMapper lessonPlansDOMapper;
@Test
public void test() throws Exception {
LessonPlansDO lessonPlansDO = lessonPlansDOMapper.selectById(58);
Map<String, Object> stringObjectMap = JsonUtils.parseMap(lessonPlansDO.getContentDetails(), String.class, Object.class);
for (Map.Entry<String, Object> entry : stringObjectMap.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
System.out.println("------------------------");
}
}
}

View File

@@ -0,0 +1,18 @@
package com.yinlihupo.enlish.service.service.plan;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class PlanTest {
@Resource
private LessonPlansService lessonPlansService;
@Test
public void test() {
lessonPlansService.generateLessonPlans(2, 146);
}
}

View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.13.2",
"echarts": "^6.0.0",
"element-plus": "^2.12.0",
"flowbite": "^1.8.1",
"nprogress": "^0.2.0",
@@ -1647,6 +1648,16 @@
"node": ">= 0.4"
}
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
@@ -3088,6 +3099,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
@@ -3420,6 +3437,15 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
}
}
}

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"axios": "^1.13.2",
"echarts": "^6.0.0",
"element-plus": "^2.12.0",
"flowbite": "^1.8.1",
"nprogress": "^0.2.0",

View File

@@ -55,6 +55,12 @@ export function generateExamWords(data) {
});
}
export function getStudentExamHistory(studentId) {
return axios.post('/exam/words/student/history', {
studentId: studentId
})
}
const resolveBlob = (res, fileName) => {
// 创建 Blob 对象,可以指定 type也可以让浏览器自动推断
const blob = new Blob([res], { type: 'application/octet-stream' });

View File

@@ -0,0 +1,69 @@
import axios from "@/axios";
export function generateLessonPlan(studentId, unitId) {
return axios.post('/plan/generate', {
studentId: studentId,
unitId: unitId
})
}
export function downloadLessonPlan(data) {
return axios.post('/plan/download', data, {
// 1. 重要:必须指定响应类型为 blob否则下载的文件会损坏乱码
responseType: 'blob',
headers: {
'Content-Type': 'application/json; application/octet-stream' // 根据需要调整
}
}).then(response => {
// 2. 提取文件名 (处理后端设置的 Content-Disposition)
// 后端示例: header("Content-Disposition", "attachment; filename*=UTF-8''" + fileName)
let fileName = 'download.zip'; // 默认兜底文件名
const contentDisposition = response.headers['content-disposition'];
if (contentDisposition) {
// 正则提取 filename*=utf-8''xxx.zip 或 filename="xxx.zip"
const fileNameMatch = contentDisposition.match(/filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/);
if (fileNameMatch && fileNameMatch[1]) {
// 后端如果用了 URLEncoder这里需要 decode
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
// 3. 开始下载流程
resolveBlob(response.data, fileName);
}).catch(error => {
console.error('下载失败', error);
showMessage('下载失败' + error, 'error');
// 注意:如果后端报错返回 JSON因为 responseType 是 blob
// 这里看到的 error.response.data 也是 blob需要转回文本才能看到错误信息
const reader = new FileReader();
reader.readAsText(error.response.data);
reader.onload = () => {
console.log(JSON.parse(reader.result)); // 打印后端实际报错
}
});
}
const resolveBlob = (res, fileName) => {
// 创建 Blob 对象,可以指定 type也可以让浏览器自动推断
const blob = new Blob([res], { type: 'application/octet-stream' });
// 兼容 IE/Edge (虽然现在很少用了)
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, fileName);
} else {
// 创建一个临时的 URL 指向 Blob
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
// 触发点击
link.style.display = 'none';
document.body.appendChild(link);
link.click();
// 清理资源
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
}
};

View File

@@ -0,0 +1,16 @@
import axios from "@/axios";
export function findStudentLessonPlans(page, size, name) {
return axios.post('/studentLessonPlans/list', {
page: page,
size: size,
name: name ?? ''
})
}
export function finishLessonPlan(studentId, planId) {
return axios.post('/studentLessonPlans/finish', {
studentId: studentId,
planId: planId
})
}

View File

@@ -0,0 +1,21 @@
import axios from "@/axios";
export function getUnitList(page, size) {
return axios.post('/unit/list', {
page: page,
size: size
})
}
export function addUnit(name, gradeId) {
return axios.post('/unit/add', {
title: name,
gradeId: gradeId
})
}
export function deleteUnit(id) {
return axios.post('/unit/delete', {
id: id
})
}

View File

@@ -0,0 +1,86 @@
<template>
<el-dialog v-model="visible" title="新增单元" width="480px" :close-on-click-modal="false">
<div class="space-y-4" v-loading="loading">
<el-form label-width="80px">
<el-form-item label="单元名称">
<el-input v-model="name" placeholder="请输入单元名称Unit 1" clearable />
</el-form-item>
<el-form-item label="年级">
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px">
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
</el-select>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { getGradeList } from '@/api/grade'
import { addUnit } from '@/api/unit'
const props = defineProps({
modelValue: { type: Boolean, default: false },
defaultGradeId: { type: [Number, String], default: null }
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const loading = ref(false)
const name = ref('')
const gradeId = ref(null)
const gradeOptions = ref([])
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
async function fetchGrades() {
loading.value = true
try {
const res = await getGradeList(1, 100)
const d = res?.data
gradeOptions.value = Array.isArray(d?.data) ? d.data : []
if (props.defaultGradeId && !gradeId.value) {
gradeId.value = Number(props.defaultGradeId)
}
} finally {
loading.value = false
}
}
async function handleSubmit() {
if (!canSubmit.value) return
loading.value = true
try {
await addUnit(name.value.trim(), Number(gradeId.value))
ElMessage.success('新增单元成功')
emit('success')
visible.value = false
} finally {
loading.value = false
}
}
watch(
() => props.modelValue,
(v) => {
if (v) {
name.value = ''
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
fetchGrades()
}
}
)
</script>
<style scoped></style>

View File

@@ -45,22 +45,11 @@
</router-link>
</li>
<li>
<a href="#"
<router-link
to="/learningplan"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
Marketplace
</a>
</li>
<li>
<a href="#"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
Features
</a>
</li>
<li>
<a href="#"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
Team
</a>
学案
</router-link>
</li>
<li>
<router-link

View File

@@ -0,0 +1,88 @@
<template>
<el-dialog v-model="visible" title="生成学案" width="520px" :close-on-click-modal="false">
<div class="space-y-4" v-loading="loading">
<el-form label-width="80px">
<el-form-item label="单元">
<el-select v-model="unitId" placeholder="请选择单元" style="width: 300px" filterable>
<el-option
v-for="u in unitOptions"
:key="u.id"
:label="u.title"
:value="u.id"
/>
</el-select>
</el-form-item>
</el-form>
<div class="text-sm text-gray-500">
学生ID{{ studentId }}
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!unitId" @click="handleGenerate">生成</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { getUnitList } from '@/api/unit'
import { generateLessonPlan } from '@/api/plan'
import { showMessage } from '../../composables/util'
const props = defineProps({
modelValue: { type: Boolean, default: false },
studentId: { type: [Number, String], required: true }
})
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const loading = ref(false)
const unitId = ref(null)
const unitOptions = ref([])
async function fetchUnits() {
loading.value = true
try {
const res = await getUnitList(1, 100)
const d = res?.data
if (d.success) {
unitOptions.value = Array.isArray(d?.data) ? d.data : []
} else {
}
} finally {
loading.value = false
}
}
async function handleGenerate() {
if (!unitId.value || !props.studentId) return
const res = await generateLessonPlan(Number(props.studentId), Number(unitId.value))
const d = res?.data
if (d.success) {
ElMessage.success('生成学案任务已提交,请等待十分钟')
visible.value = false
} else {
showMessage(d.message || '生成学案失败,请联系管理员', 'error')
visible.value = false
}
}
watch(
() => props.modelValue,
(v) => {
if (v) {
unitId.value = null
fetchUnits()
}
}
)
</script>
<style scoped></style>

View File

@@ -0,0 +1,155 @@
<template>
<div style="width: 100%; height: 260px;">
<div ref="elRef" style="width: 100%; height: 100%;"></div>
</div>
<ExamWordsDetailCard v-model="detailVisible" :id="detailId" />
</template>
<script setup>
import { defineProps, ref, onMounted, onUnmounted, watch } from 'vue'
import ExamWordsDetailCard from '@/layouts/components/ExamWordsDetailCard.vue'
const props = defineProps({
data: {
type: Array,
default: () => []
}
})
const elRef = ref(null)
let chart = null
let echartsLib = null
const detailVisible = ref(false)
const detailId = ref(null)
function sortData(arr) {
return Array.isArray(arr)
? arr.slice().sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())
: []
}
function toSource(arr) {
return sortData(arr).map(it => ({
startDate: it.startDate,
correctWordCount: Number(it.correctWordCount) || 0,
wrongWordCount: Number(it.wrongWordCount) || 0,
examWordsId: it.examWordsId ?? null,
id: it.id ?? null
}))
}
function buildOption(source) {
return {
dataset: [
{
id: 'dataset_history',
source: source
}
],
tooltip: {
trigger: 'axis'
},
legend: {
top: 8
},
xAxis: {
type: 'category',
nameLocation: 'middle'
},
yAxis: {
name: 'Word Count',
min: 0
},
series: [
{
type: 'line',
datasetId: 'dataset_history',
showSymbol: false,
name: '正确词数',
encode: {
x: 'startDate',
y: 'correctWordCount',
itemName: 'startDate',
tooltip: ['correctWordCount']
}
},
{
type: 'line',
datasetId: 'dataset_history',
showSymbol: false,
name: '错误词数',
encode: {
x: 'startDate',
y: 'wrongWordCount',
itemName: 'startDate',
tooltip: ['wrongWordCount']
}
}
]
}
}
function resize() {
if (chart) chart.resize()
}
async function ensureEcharts() {
try {
const mod = await import('echarts')
echartsLib = mod
} catch (e) {
if (window.echarts) {
echartsLib = window.echarts
} else {
await new Promise(resolve => {
const s = document.createElement('script')
s.src = 'https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js'
s.onload = resolve
document.head.appendChild(s)
})
echartsLib = window.echarts
}
}
}
async function render() {
if (!elRef.value) return
await ensureEcharts()
if (!echartsLib) return
if (!chart) {
chart = echartsLib.init(elRef.value)
chart.on('click', handlePointClick)
window.addEventListener('resize', resize)
}
const source = toSource(props.data)
const option = buildOption(source)
chart.setOption(option)
}
function handlePointClick(params) {
const row = params?.data
console.log(row)
const id = row?.id ?? null
if (id !== null && id !== undefined) {
detailId.value = id
detailVisible.value = true
}
}
onMounted(() => {
render()
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
if (chart) {
chart.off('click', handlePointClick)
chart.dispose()
chart = null
}
})
watch(() => props.data, () => {
render()
}, { deep: true })
</script>

View File

@@ -0,0 +1,164 @@
<template>
<div class="common-layout">
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-main class="p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-lg font-semibold mb-4">学案查询</div>
<div class="flex flex-wrap items-center gap-3 mb-4">
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button @click="onReset">重置</el-button>
</div>
<el-table ref="tableRef" :data="rows" border class="w-full" v-loading="loading" row-key="id">
<el-table-column type="expand">
<template #default="{ row }">
<div class="p-3">
<div class="text-sm font-semibold mb-2">学案</div>
<el-table :data="row.plans || []" size="small" border>
<el-table-column prop="id" label="计划ID" width="100" />
<el-table-column prop="title" label="标题" min-width="280" />
<el-table-column label="状态" width="120">
<template #default="{ row: plan }">
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row: plan }">
<el-button
type="primary"
size="small"
:loading="downloadingIds.includes(plan.id)"
@click="onDownload(plan)"
>下载</el-button>
<el-button
class="ml-2"
type="success"
size="small"
:disabled="plan.isFinished === 1"
:loading="finishingIds.includes(plan.id)"
@click="onFinish(row.id, plan.id, plan)"
>完成</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="id" label="学生ID" width="100" />
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="className" label="班级" min-width="120" />
<el-table-column prop="gradeName" label="年级" min-width="120" />
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
@size-change="handleSizeChange" />
</div>
</div>
</el-main>
</el-container>
</div>
</template>
<script setup>
import Header from '@/layouts/components/Header.vue'
import { ref, onMounted } from 'vue'
import { findStudentLessonPlans, finishLessonPlan } from '@/api/studentLessonPlans'
import { downloadLessonPlan } from '@/api/plan'
import { showMessage } from '@/composables/util'
const rows = ref([])
const loading = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const searchName = ref('')
const tableRef = ref(null)
const downloadingIds = ref([])
const finishingIds = ref([])
async function fetchLessonPlans() {
loading.value = true
try {
const res = await findStudentLessonPlans(pageNo.value, pageSize.value, searchName.value || '')
const d = res.data
rows.value = Array.isArray(d.data) ? d.data : []
totalCount.value = d.totalCount || 0
pageNo.value = d.pageNo || pageNo.value
pageSize.value = d.pageSize || pageSize.value
} finally {
loading.value = false
}
}
function handlePageChange(p) {
pageNo.value = p
fetchLessonPlans()
}
function handleSizeChange(s) {
pageSize.value = s
pageNo.value = 1
fetchLessonPlans()
}
function onSearch() {
pageNo.value = 1
fetchLessonPlans()
}
function onReset() {
searchName.value = ''
pageNo.value = 1
fetchLessonPlans()
}
async function onDownload(plan) {
if (!plan?.id) {
showMessage('无效的计划ID', 'error')
return
}
if (!downloadingIds.value.includes(plan.id)) {
downloadingIds.value = [...downloadingIds.value, plan.id]
}
try {
await downloadLessonPlan({ id: plan.id })
showMessage('开始下载', 'success')
} finally {
downloadingIds.value = downloadingIds.value.filter(id => id !== plan.id)
}
}
async function onFinish(studentId, planId, plan) {
if (!studentId || !planId) {
showMessage('参数错误', 'error')
return
}
if (!finishingIds.value.includes(planId)) {
finishingIds.value = [...finishingIds.value, planId]
}
try {
const res = await finishLessonPlan(studentId, planId)
const d = res.data
if (d?.success !== false) {
plan.isFinished = 1
showMessage('已标记完成', 'success')
} else {
showMessage(d?.message || '标记失败', 'error')
}
} catch (e) {
showMessage('标记失败', 'error')
} finally {
finishingIds.value = finishingIds.value.filter(id => id !== planId)
}
}
onMounted(() => {
fetchLessonPlans()
})
</script>

View File

@@ -35,7 +35,7 @@
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-2">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-1">
<div class="text-lg font-semibold mb-4">学生查询</div>
<div class="flex flex-wrap items-center gap-3 mb-4">
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
@@ -49,6 +49,10 @@
@click="showGenerateDialog = true">
生成试题
</el-button>
<el-button type="warning" :disabled="selectedStudentIds.length !== 1"
@click="showLessonPlanDialog = true">
生成学案
</el-button>
</div>
<el-table ref="studentTableRef" :data="students" border class="w-full"
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
@@ -57,8 +61,9 @@
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="classId" label="班级ID" width="100" />
<el-table-column prop="gradeId" label="年级ID" width="100" />
<el-table-column label="操作" width="120" fixed="right">
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
</template>
</el-table-column>
@@ -79,6 +84,10 @@
:default-grade-id="selectedGradeId"
@success="fetchStudents"
/>
<LessonPlanDialog
v-model="showLessonPlanDialog"
:student-id="selectedStudentIds[0]"
/>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="gradeLoading">
@@ -105,6 +114,40 @@
<AddGradeDialog v-model="showAddGradeDialog" @success="fetchGrades" />
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="unitLoading">
<div class="text-lg font-semibold mb-4">单元列表</div>
<el-table ref="unitTableRef" :data="units" border class="w-full">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="单元名称" min-width="200" />
<el-table-column prop="version" label="版本" min-width="120" />
<el-table-column prop="createAt" label="创建时间" min-width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="danger" size="small" @click.stop="onDeleteUnit(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination
background
layout="prev, pager, next, sizes, total"
:total="unitTotalCount"
:page-size="unitPageSize"
:current-page="unitPageNo"
@current-change="handleUnitPageChange"
@size-change="handleUnitSizeChange"
/>
</div>
<div class="mt-3 flex justify-end">
<el-button type="primary" :disabled="!selectedGradeId" @click="showAddUnitDialog = true">新增单元</el-button>
</div>
<AddUnitDialog
v-model="showAddUnitDialog"
:default-grade-id="selectedGradeId"
@success="fetchUnits"
/>
</div>
</div>
</el-main>
@@ -116,12 +159,16 @@
import Header from '@/layouts/components/Header.vue'
import { ref, onMounted } from 'vue'
import { getClassList, deleteClass } from '@/api/class'
import { getGradeList } from '@/api/grade'
import { getGradeList, deleteGrade } from '@/api/grade'
import { getStudentList, deleteStudent } from '@/api/student'
import ExamGenerateDialog from '@/layouts/components/ExamGenerateDialog.vue'
import AddClassDialog from '@/layouts/components/AddClassDialog.vue'
import AddGradeDialog from '@/layouts/components/AddGradeDialog.vue'
import AddStudentDialog from '@/layouts/components/AddStudentDialog.vue'
import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
import { getUnitList, deleteUnit } from '@/api/unit'
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
import { useRouter } from 'vue-router'
const classes = ref([])
const pageNo = ref(1)
@@ -153,6 +200,16 @@ const studentTableRef = ref(null)
const selectedStudentIds = ref([])
const showGenerateDialog = ref(false)
const showAddStudentDialog = ref(false)
const showLessonPlanDialog = ref(false)
const units = ref([])
const unitPageNo = ref(1)
const unitPageSize = ref(10)
const unitTotalCount = ref(0)
const unitLoading = ref(false)
const unitTableRef = ref(null)
const showAddUnitDialog = ref(false)
const router = useRouter()
async function fetchClasses() {
loading.value = true
@@ -235,6 +292,9 @@ function handleStudentSizeChange(s) {
function onStudentSelectionChange(rows) {
selectedStudentIds.value = rows.map(r => r.id)
}
function onViewStudent(row) {
router.push(`/student/${row.id}`)
}
function onClassRowClick(row) {
selectedClassId.value = row.id
selectedClassTitle.value = row.title
@@ -303,9 +363,48 @@ async function onDeleteGrade(row) {
}
}
async function fetchUnits() {
unitLoading.value = true
try {
const res = await getUnitList(unitPageNo.value, unitPageSize.value)
const d = res.data
units.value = Array.isArray(d.data) ? d.data : []
unitTotalCount.value = d.totalCount || 0
unitPageNo.value = d.pageNo || unitPageNo.value
unitPageSize.value = d.pageSize || unitPageSize.value
} finally {
unitLoading.value = false
}
}
function handleUnitPageChange(p) {
unitPageNo.value = p
fetchUnits()
}
function handleUnitSizeChange(s) {
unitPageSize.value = s
unitPageNo.value = 1
fetchUnits()
}
async function onDeleteUnit(row) {
try {
const res = await deleteUnit(row.id)
const data = res.data
console.log(data)
if (data.success) {
ElMessage.success('删除成功')
} else {
ElMessage.error(data.message || '删除失败')
}
await fetchUnits()
} catch (e) {
ElMessage.error('删除失败')
}
}
onMounted(() => {
fetchClasses()
fetchGrades()
fetchStudents()
fetchUnits()
})
</script>

View File

@@ -0,0 +1,77 @@
<template>
<div class="common-layout">
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-main class="p-4">
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6"
v-loading="loading">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-lg font-semibold mb-4">学生详情</div>
<template v-if="detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
</el-descriptions>
</template>
<template v-else>
<el-empty description="请从班级页跳转" />
</template>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-md font-semibold mb-3">学生考试记录</div>
<ExamHistoryChart :data="history" />
</div>
</div>
</el-main>
</el-container>
</div>
</template>
<script setup>
import Header from '@/layouts/components/Header.vue'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getStudentDetail } from '@/api/student'
import { getStudentExamHistory } from '@/api/exam'
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
const loading = ref(false)
const detail = ref(null)
const route = useRoute()
const history = ref([])
async function fetchDetail() {
const id = route.params.id
if (!id) return
loading.value = true
try {
const res = await getStudentDetail(id)
const d = res.data
detail.value = d?.data || null
} finally {
loading.value = false
}
}
async function fetchExamHistory() {
const id = route.params.id
if (!id) return
const res = await getStudentExamHistory(id)
const d = res.data
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
}) : []
}
onMounted(() => {
fetchDetail()
fetchExamHistory()
})
</script>

View File

@@ -1,7 +1,9 @@
import Index from '@/pages/index.vue'
import Uploadpng from '@/pages/uploadpng.vue'
import LearningPlan from '@/pages/LearningPlan.vue'
import Class from '@/pages/class.vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import Student from '@/pages/student.vue'
// 统一在这里声明所有路由
const routes = [
@@ -18,6 +20,20 @@ const routes = [
meta: { // meta 信息
title: '上传图片' // 页面标题
}
},
{
path: '/learningplan', // 路由地址
component: LearningPlan, // 对应组件
meta: { // meta 信息
title: '学案' // 页面标题
}
},
{
path: '/student/:id', // 路由地址
component: Student, // 对应组件
meta: { // meta 信息
title: '学生详情' // 页面标题
}
}
]