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

@@ -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]