feat(plan): 添加学生学案查看及下载功能
- 在学生列表表格中新增“查看学案”按钮,支持查看对应学生的学案列表 - 新增StudentPlanListDialog组件,实现学案列表展示和学案文件下载 - 后端新增查询学生学案接口,支持按学生ID获取未完成学案列表 - 后端数据层和服务层添加按学生ID查询学案的方法 - 调整计划生成相关逻辑,优化学案数据字段命名 - Vue前端调用新增接口,实现学生学案列表动态加载与下载操作 - 完善学案状态显示和列表交互体验
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -10,4 +10,6 @@ public interface LessonPlansService {
|
||||
List<LessonPlansDO> findLessonPlans(List<Integer> ids);
|
||||
|
||||
LessonPlansDO findLessonPlanById(Integer id);
|
||||
|
||||
List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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' });
|
||||
|
||||
96
enlish-vue/src/layouts/components/StudentPlanListDialog.vue
Normal file
96
enlish-vue/src/layouts/components/StudentPlanListDialog.vue
Normal 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>
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user