feat(plan): 添加学生学案历史查询及展示功能

- 新增FindStudentPlanHistoryReqVO和FindStudentPlanHistoryListRspVO数据对象
- 将LessonPlansDO及StudentLessonPlansDO中日期类型由Date改为LocalDateTime
- LessonPlansServiceImpl中创建时间使用LocalDateTime.now()
- StudentLessonPlansService及实现类添加按学生ID查询学案历史接口
- StudentLessonPlansDOMapper及XML添加按学生ID查询学案历史SQL映射
- StudentLessonPlansController新增/history接口返回学生学案历史列表
- 前端student.vue新增学案历史图表PlanHistoryChart组件展示学案历史数据
- 新增PlanHistoryChart.vue组件,实现基于echarts的学案历史折线图
- studentLessonPlans.js新增findStudentPlanHistory接口调用后端学案历史数据接口
This commit is contained in:
lbw
2025-12-18 12:43:48 +08:00
parent a50c9a2b16
commit 7a66548aed
13 changed files with 228 additions and 9 deletions

View File

@@ -3,10 +3,7 @@ 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.model.vo.plan.*;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.service.StudentLessonPlansService;
import com.yinlihupo.enlish.service.service.StudentService;
@@ -77,7 +74,7 @@ public class StudentLessonPlansController {
return PageResponse.success(findStudentPlansRspVOList, page, studentLessonPlanTotal, size);
}
@PostMapping("/finish")
@PostMapping("finish")
@ApiOperationLog(description = "完成学案")
public Response<String> finishStudentPlan(@RequestBody FinishStudentPlanReqVO finishStudentPlanReqVO) {
Integer studentId = finishStudentPlanReqVO.getStudentId();
@@ -89,4 +86,22 @@ public class StudentLessonPlansController {
}
return Response.fail("完成学案失败");
}
@PostMapping("history")
@ApiOperationLog(description = "查询学案历史")
public Response<List<FindStudentPlanHistoryListRspVO>> findStudentPlanHistory(@RequestBody FindStudentPlanHistoryReqVO findStudentPlanHistoryReqVO) {
Integer studentId = findStudentPlanHistoryReqVO.getStudentId();
List<FindStudentPlanHistoryListRspVO> list = studentLessonPlansService.findStudentLessonPlansByStudentId(studentId)
.stream().map(studentLessonPlansDO -> FindStudentPlanHistoryListRspVO.builder()
.id(studentLessonPlansDO.getId())
.studentId(studentLessonPlansDO.getStudentId())
.planId(studentLessonPlansDO.getPlanId())
.startTime(studentLessonPlansDO.getStartTime())
.masteryRat(studentLessonPlansDO.getMasteryRat())
.totalCount(studentLessonPlansDO.getTotalCount())
.build()
).toList();
return Response.success(list);
}
}

View File

@@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Date;
@AllArgsConstructor
@@ -20,7 +21,7 @@ public class LessonPlansDO {
private Integer unitId;
private Date createdAt;
private LocalDateTime createdAt;
private String contentDetails;

View File

@@ -6,6 +6,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
@AllArgsConstructor
@@ -19,7 +20,7 @@ public class StudentLessonPlansDO {
private Integer planId;
private Date startTime;
private LocalDateTime startTime;
private BigDecimal masteryRat;

View File

@@ -14,4 +14,6 @@ public interface StudentLessonPlansDOMapper {
Integer selectStudentPlanTotal();
Integer finfishStudentPlan(@Param("studentId") Integer studentId, @Param("planId") Integer planId, @Param("count") Integer count, @Param("mastery") double mastery);
List<StudentLessonPlansDO> selectStudentLessonPlanListByStudentId(@Param("studentId") Integer studentId);
}

View File

@@ -0,0 +1,29 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentPlanHistoryListRspVO {
private Integer id;
private Integer studentId;
private Integer planId;
private LocalDateTime startTime;
private BigDecimal masteryRat;
private Integer totalCount;
}

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 FindStudentPlanHistoryReqVO {
private Integer studentId;
}

View File

@@ -11,4 +11,6 @@ public interface StudentLessonPlansService {
Integer findStudentLessonPlanTotal();
int finishStudentLessonPlan(Integer studentId, Integer planId);
List<StudentLessonPlansDO> findStudentLessonPlansByStudentId(Integer studentId);
}

View File

@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
@Service
@@ -104,7 +105,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
.title(lessonPlanMap.get("title").toString())
.gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId())
.createdAt(new Date())
.createdAt(LocalDateTime.now())
.contentDetails(JsonUtils.toJsonString(lessonPlanMap))
.build();
lessonPlansDOMapper.insert(lessonPlansDO);
@@ -132,7 +133,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
.title(map.get("title").toString())
.gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId())
.createdAt(new Date())
.createdAt(LocalDateTime.now())
.contentDetails(JsonUtils.toJsonString(map))
.build();
lessonPlansDOMapper.insert(lessonPlansDO);

View File

@@ -44,4 +44,9 @@ public class StudentLessonPlansServiceImpl implements StudentLessonPlansService
return studentLessonPlansDOMapper.finfishStudentPlan(studentId, planId, wordStrengthCount, (double) wordStrengthCount / wordTotal);
}
@Override
public List<StudentLessonPlansDO> findStudentLessonPlansByStudentId(Integer studentId) {
return studentLessonPlansDOMapper.selectStudentLessonPlanListByStudentId(studentId);
}
}

View File

@@ -45,6 +45,12 @@
SELECT count(DISTINCT student_id) AS total
FROM student_lesson_plans;
</select>
<select id="selectStudentLessonPlanListByStudentId" resultMap="BaseResultMap">
SELECT *
FROM student_lesson_plans
WHERE student_id = #{studentId}
limit 500
</select>
<update id="finfishStudentPlan">
update student_lesson_plans

View File

@@ -14,3 +14,9 @@ export function finishLessonPlan(studentId, planId) {
planId: planId
})
}
export function findStudentPlanHistory(studentId) {
return axios.post('/studentLessonPlans/history', {
studentId: studentId
})
}

View File

@@ -0,0 +1,131 @@
<template>
<div style="width: 100%; height: 260px;">
<div ref="elRef" style="width: 100%; height: 100%;"></div>
</div>
</template>
<script setup>
import { defineProps, ref, onMounted, onUnmounted, watch } from 'vue'
import { findStudentPlanHistory } from '@/api/studentLessonPlans'
const props = defineProps({
studentId: { type: [Number, String], required: true }
})
const elRef = ref(null)
let chart = null
let echartsLib = null
const rows = ref([])
function sortData(arr) {
return Array.isArray(arr)
? arr.slice().sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime())
: []
}
function toSource(arr) {
return sortData(arr).map(it => ({
startTime: it.startTime,
totalCount: Number(it.totalCount) || 0,
planId: it.planId ?? null,
id: it.id ?? null
}))
}
function buildOption(source) {
return {
dataset: [
{
id: 'dataset_history',
source: source
}
],
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
nameLocation: 'middle'
},
yAxis: {
name: 'Total Count',
min: 0
},
series: [
{
type: 'line',
datasetId: 'dataset_history',
showSymbol: false,
encode: {
x: 'startTime',
y: 'totalCount',
itemName: 'startTime',
tooltip: ['totalCount']
}
}
]
}
}
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)
window.addEventListener('resize', resize)
}
const source = toSource(rows.value)
const option = buildOption(source)
chart.setOption(option)
}
async function fetch() {
if (props.studentId === undefined || props.studentId === null) return
const res = await findStudentPlanHistory(Number(props.studentId))
const d = res?.data?.data ?? []
rows.value = Array.isArray(d) ? d : []
}
onMounted(async () => {
await fetch()
await render()
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
if (chart) {
chart.dispose()
chart = null
}
})
watch(() => props.studentId, async () => {
await fetch()
await render()
})
</script>

View File

@@ -27,6 +27,10 @@
<div class="text-md font-semibold mb-3">学生考试记录</div>
<ExamHistoryChart :data="history" />
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-md font-semibold mb-3">学生学案记录</div>
<PlanHistoryChart :student-id="route.params.id" />
</div>
</div>
</el-main>
@@ -41,6 +45,7 @@ import { useRoute } from 'vue-router'
import { getStudentDetail } from '@/api/student'
import { getStudentExamHistory } from '@/api/exam'
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
const loading = ref(false)
const detail = ref(null)