feat(plan): 支持学案生成状态轮询与进度显示
- 新增接口检查学案是否正在生成,防止重复生成任务 - 使用 Redis 缓存标识学案生成状态,设置 12 分钟过期时间 - 生成学案时记录状态至 Redis,生成完成后自动清除 - Vue 学案列表新增学案生成进度条显示与已生成标签 - 新增组件事件监听生成成功,触发轮询检测学案状态 - 轮询间隔 10 秒,动态更新学案生成进度,最高至 95% - 路由离开与组件卸载时停止所有轮询,防止内存泄漏 - 优化学案生成逻辑,新增小测试卷自动关联及数据入库 - 更新配置文件模板路径,提高文档管理一致性
This commit is contained in:
@@ -51,6 +51,12 @@ export function getLessonPlanWords(planId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function checkIsGenerated(studentId) {
|
||||
return axios.post('plan/check', {
|
||||
studentId: studentId
|
||||
})
|
||||
}
|
||||
|
||||
const resolveBlob = (res, fileName) => {
|
||||
// 创建 Blob 对象,可以指定 type,也可以让浏览器自动推断
|
||||
const blob = new Blob([res], { type: 'application/octet-stream' });
|
||||
|
||||
@@ -39,7 +39,7 @@ const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
studentId: { type: [Number, String], required: true }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -71,6 +71,7 @@ async function handleGenerate() {
|
||||
const d = res?.data
|
||||
if (d.success) {
|
||||
ElMessage.success('生成学案任务已提交,请等待十分钟')
|
||||
emit('success', { studentId: Number(props.studentId) })
|
||||
visible.value = false
|
||||
} else {
|
||||
showMessage(d.message || '生成学案失败,请联系管理员', 'error')
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<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 }">
|
||||
@@ -50,7 +49,6 @@
|
||||
</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" />
|
||||
|
||||
@@ -59,6 +59,18 @@
|
||||
<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-column prop="phone" label="学案" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="generatingPercents[row.id] !== undefined">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="info" effect="plain">已生成</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
|
||||
@@ -85,6 +97,7 @@
|
||||
<LessonPlanDialog
|
||||
v-model="showLessonPlanDialog"
|
||||
:student-id="selectedStudentIds[0]"
|
||||
@success="onLessonPlanGenerateSuccess"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +123,7 @@
|
||||
|
||||
<script setup>
|
||||
import Header from '@/layouts/components/Header.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getClassList, deleteClass } from '@/api/class'
|
||||
import { getGradeList, deleteGrade } from '@/api/grade'
|
||||
import { getStudentList, deleteStudent } from '@/api/student'
|
||||
@@ -121,8 +134,9 @@ 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'
|
||||
import { useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { checkIsGenerated } from '@/api/plan'
|
||||
|
||||
const classes = ref([])
|
||||
const pageNo = ref(1)
|
||||
@@ -155,6 +169,8 @@ const selectedStudentIds = ref([])
|
||||
const showGenerateDialog = ref(false)
|
||||
const showAddStudentDialog = ref(false)
|
||||
const showLessonPlanDialog = ref(false)
|
||||
const generatingPercents = ref({})
|
||||
const pollingTimers = {}
|
||||
|
||||
const units = ref([])
|
||||
const unitPageNo = ref(1)
|
||||
@@ -209,6 +225,7 @@ async function fetchStudents() {
|
||||
studentTotalCount.value = d.totalCount || 0
|
||||
studentPageNo.value = d.pageNo || studentPageNo.value
|
||||
studentPageSize.value = d.pageSize || studentPageSize.value
|
||||
ensurePollingForCurrentStudents()
|
||||
} finally {
|
||||
studentLoading.value = false
|
||||
}
|
||||
@@ -263,6 +280,57 @@ function onGradeRowClick(row) {
|
||||
studentPageNo.value = 1
|
||||
fetchStudents()
|
||||
}
|
||||
|
||||
function startLessonPlanPolling(studentId) {
|
||||
if (!studentId) return
|
||||
if (pollingTimers[studentId]) return
|
||||
pollingTimers[studentId] = setInterval(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 {
|
||||
if (generatingPercents.value[studentId] !== undefined) {
|
||||
delete generatingPercents.value[studentId]
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const p = Number(generatingPercents.value[studentId]) || 1
|
||||
generatingPercents.value[studentId] = Math.min(p + 3, 95)
|
||||
}
|
||||
}, 10000)
|
||||
}
|
||||
function onLessonPlanGenerateSuccess(payload) {
|
||||
const sid = payload?.studentId || selectedStudentIds.value?.[0]
|
||||
startLessonPlanPolling(sid)
|
||||
}
|
||||
function ensurePollingForCurrentStudents() {
|
||||
(students.value || []).forEach(s => startLessonPlanPolling(s.id))
|
||||
}
|
||||
function stopPolling(studentId) {
|
||||
const t = pollingTimers[studentId]
|
||||
if (t) {
|
||||
clearInterval(t)
|
||||
delete pollingTimers[studentId]
|
||||
}
|
||||
if (generatingPercents.value[studentId] !== undefined) {
|
||||
delete generatingPercents.value[studentId]
|
||||
}
|
||||
}
|
||||
function stopAllPolling() {
|
||||
Object.keys(pollingTimers).forEach(id => stopPolling(id))
|
||||
}
|
||||
onUnmounted(() => {
|
||||
stopAllPolling()
|
||||
})
|
||||
onBeforeRouteLeave(() => {
|
||||
stopAllPolling()
|
||||
})
|
||||
async function onDeleteStudent(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该学生?', '提示', {
|
||||
|
||||
Reference in New Issue
Block a user