feat(plan): 添加学生学案查看及下载功能

- 在学生列表表格中新增“查看学案”按钮,支持查看对应学生的学案列表
- 新增StudentPlanListDialog组件,实现学案列表展示和学案文件下载
- 后端新增查询学生学案接口,支持按学生ID获取未完成学案列表
- 后端数据层和服务层添加按学生ID查询学案的方法
- 调整计划生成相关逻辑,优化学案数据字段命名
- Vue前端调用新增接口,实现学生学案列表动态加载与下载操作
- 完善学案状态显示和列表交互体验
This commit is contained in:
lbw
2025-12-31 16:20:52 +08:00
parent 868e0bb7bd
commit 36e5231c6c
10 changed files with 183 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.utils.TTSUtil;
import com.yinlihupo.enlish.service.utils.WordExportUtil;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.PageResponse;
import com.yinlihupo.framework.common.response.Response;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
@@ -116,5 +117,19 @@ public class LessonPlanController {
return Response.success("学案生成完成");
}
@PostMapping("student/list")
@ApiOperationLog(description = "查询学生学案")
public Response<FindPlanStudentListRspVO> findStudentPlans(@RequestBody FindPlanStudentReqVO findPlanStudentReqVO) {
List<LessonPlansDO> lessonPlansDOS = lessonPlanService.findLessonPlansByStudentId(findPlanStudentReqVO.getStudentId());
List<LessonPlanItem> list = lessonPlansDOS.stream().map(lessonPlansDO -> LessonPlanItem
.builder()
.id(lessonPlansDO.getId())
.isFinished(0)
.title(lessonPlansDO.getTitle())
.build())
.toList();
return Response.success(FindPlanStudentListRspVO.builder().lessonPlanItems(list).build());
}
}

View File

@@ -14,4 +14,6 @@ public interface LessonPlansDOMapper {
List<LessonPlansDO> findLessonPlansByStudentId(@Param("ids") List<Integer> ids);
LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId);
List<LessonPlansDO> selectByStudentId(@Param("studentId") Integer studentId);
}

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;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindPlanStudentListRspVO {
List<LessonPlanItem> lessonPlanItems;
}

View File

@@ -0,0 +1,14 @@
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 FindPlanStudentReqVO {
private Integer studentId;
}

View File

@@ -10,4 +10,6 @@ public interface LessonPlansService {
List<LessonPlansDO> findLessonPlans(List<Integer> ids);
LessonPlansDO findLessonPlanById(Integer id);
List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId);
}

View File

@@ -192,6 +192,11 @@ public class LessonPlansServiceImpl implements LessonPlansService {
return lessonPlansDOMapper.selectByLessonId(id);
}
@Override
public List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId) {
return lessonPlansDOMapper.selectByStudentId(studentId);
}
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> words,
int day,
@@ -218,7 +223,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("studentId", studentId);
data.put("studentStr", studentDO.getName());
data.put("examStr", ExamTitle);
data.put("words", words);
data.put("checkList", words);
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// Configure config = Configure.builder()
// .bind("checkList", policy)

View File

@@ -39,4 +39,15 @@
where id = #{lessonId}
</select>
<select id="selectByStudentId" resultMap="BaseResultMap">
select *
from lesson_plans
where id in (
select student_lesson_plans.plan_id
from student_lesson_plans
where student_id = #{studentId}
and is_finished = 0
)
</select>
</mapper>

View File

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

View File

@@ -0,0 +1,96 @@
<template>
<el-dialog v-model="visible" title="学生学案列表" width="680px" :close-on-click-modal="false">
<div v-loading="loading">
<el-table :data="plans" border>
<el-table-column prop="title" label="标题" min-width="360" />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'" effect="plain">
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
:loading="downloadingIds.includes(row.id)"
@click="handleDownload(row)"
>下载</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { getPlanListByStudentId, downloadLessonPlan } 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 plans = ref([])
const downloadingIds = ref([])
async function fetchPlans() {
if (!props.studentId) return
loading.value = true
try {
const res = await getPlanListByStudentId(Number(props.studentId))
const d = res?.data
if (d?.success !== false) {
const items = d?.data?.lessonPlanItems
plans.value = Array.isArray(items) ? items : []
} else {
showMessage(d?.message || '获取学案失败', 'error')
}
} finally {
loading.value = false
}
}
async function handleDownload(row) {
if (!row?.id) {
showMessage('无效的计划ID', 'error')
return
}
if (!downloadingIds.value.includes(row.id)) {
downloadingIds.value = [...downloadingIds.value, row.id]
}
try {
await downloadLessonPlan({ id: row.id })
showMessage('开始下载', 'success')
} finally {
downloadingIds.value = downloadingIds.value.filter(id => id !== row.id)
}
}
watch(
() => props.modelValue,
(v) => {
if (v) {
fetchPlans()
}
}
)
</script>
<style scoped></style>

View File

@@ -67,7 +67,9 @@
</div>
</template>
<template v-else>
<el-tag type="info" effect="plain">已生成</el-tag>
<div class="flex items-center gap-2">
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
</div>
</template>
</template>
</el-table-column>
@@ -99,6 +101,10 @@
:student-id="selectedStudentIds[0]"
@success="onLessonPlanGenerateSuccess"
/>
<StudentPlanListDialog
v-model="showPlanListDialog"
:student-id="planStudentId"
/>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="gradeLoading">
@@ -132,6 +138,7 @@ 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 StudentPlanListDialog from '@/layouts/components/StudentPlanListDialog.vue'
import { getUnitList, deleteUnit } from '@/api/unit'
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
import { useRouter, onBeforeRouteLeave } from 'vue-router'
@@ -171,6 +178,8 @@ const showAddStudentDialog = ref(false)
const showLessonPlanDialog = ref(false)
const generatingPercents = ref({})
const pollingTimers = {}
const showPlanListDialog = ref(false)
const planStudentId = ref(null)
const units = ref([])
const unitPageNo = ref(1)
@@ -284,14 +293,12 @@ function onGradeRowClick(row) {
function startLessonPlanPolling(studentId) {
if (!studentId) return
if (pollingTimers[studentId]) return
pollingTimers[studentId] = setInterval(async () => {
const pollOnce = async () => {
try {
const res = await checkIsGenerated(studentId)
const d = res?.data
const ok = d?.success === false || d?.success === false || d === false
console.log(ok)
if (ok) {
console.log('ok', d)
const p = Number(generatingPercents.value[studentId]) || 1
generatingPercents.value[studentId] = Math.min(p + 5, 95)
} else {
@@ -303,7 +310,9 @@ function startLessonPlanPolling(studentId) {
const p = Number(generatingPercents.value[studentId]) || 1
generatingPercents.value[studentId] = Math.min(p + 3, 95)
}
}, 10000)
}
pollOnce()
pollingTimers[studentId] = setInterval(pollOnce, 10000)
}
function onLessonPlanGenerateSuccess(payload) {
const sid = payload?.studentId || selectedStudentIds.value?.[0]