- 新增FindStudentMasteryDetailReqVO和FindStudentMasteryDetailRspVO数据类 - 学生接口新增/ student/mastery/detail,用于查询学生词汇掌握详情 - StudentService及实现类添加查询词汇掌握详情的方法 - WordMasteryLogDOMapper新增selectAllByStudentId方法支持查询 - SaTokenConfigure增加对新接口的免认证配置 - 前端api新增getStudentWordMastery方法 - 学生页面新增WordMasteryHeatmap组件并展示词汇掌握热力图 - 创建WordMasteryHeatmap组件,支持动态请求数据及Echarts热力图渲染 - 热力图按记忆强度排序,提供丰富的鼠标悬停提示信息
130 lines
5.1 KiB
Vue
130 lines
5.1 KiB
Vue
<template>
|
|
<div class="common-layout">
|
|
<el-container>
|
|
<el-header>
|
|
<Header></Header>
|
|
</el-header>
|
|
|
|
<el-main class="p-4">
|
|
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6"
|
|
v-loading="loading">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div class="text-lg font-semibold mb-4">学生详情</div>
|
|
<template v-if="detail">
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
|
|
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
|
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
|
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
|
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</template>
|
|
<template v-else>
|
|
<el-empty description="请从班级页跳转" />
|
|
</template>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<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 class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
|
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="text-md font-semibold">学习分析</div>
|
|
<el-button type="primary" size="small" :loading="analyzeLoading" @click="fetchStudyAnalyze">
|
|
生成学习分析
|
|
</el-button>
|
|
</div>
|
|
<template v-if="analysisHtml">
|
|
<div class="leading-7 text-gray-700 dark:text-gray-200" v-html="analysisHtml"></div>
|
|
</template>
|
|
<template v-else>
|
|
<el-empty description="点击右上按钮生成学习分析" />
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</el-main>
|
|
|
|
</el-container>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import Header from '@/layouts/components/Header.vue'
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { getStudentDetail, getStudentStudyAnalyze } from '@/api/student'
|
|
import { getStudentExamHistory } from '@/api/exam'
|
|
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
|
|
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
|
|
import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue'
|
|
import MarkdownIt from 'markdown-it'
|
|
|
|
const loading = ref(false)
|
|
const detail = ref(null)
|
|
const route = useRoute()
|
|
const history = ref([])
|
|
const analyzeLoading = ref(false)
|
|
const analysisText = ref('')
|
|
const md = new MarkdownIt({
|
|
html: false,
|
|
linkify: true,
|
|
breaks: true
|
|
})
|
|
const analysisHtml = computed(() => {
|
|
return analysisText.value ? md.render(analysisText.value) : ''
|
|
})
|
|
|
|
async function fetchDetail() {
|
|
const id = route.params.id
|
|
if (!id) return
|
|
loading.value = true
|
|
try {
|
|
const res = await getStudentDetail(id)
|
|
const d = res.data
|
|
detail.value = d?.data || null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function fetchExamHistory() {
|
|
const id = route.params.id
|
|
if (!id) return
|
|
const res = await getStudentExamHistory(id)
|
|
const d = res.data
|
|
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
|
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
|
}) : []
|
|
}
|
|
|
|
async function fetchStudyAnalyze() {
|
|
const id = route.params.id
|
|
if (!id) return
|
|
analyzeLoading.value = true
|
|
try {
|
|
const res = await getStudentStudyAnalyze({
|
|
studentId: Number(id)
|
|
})
|
|
const d = res.data
|
|
const raw = typeof d?.data === 'string' ? d.data : ''
|
|
analysisText.value = raw.replace(/\\n/g, '\n')
|
|
} finally {
|
|
analyzeLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchDetail()
|
|
fetchExamHistory()
|
|
})
|
|
</script>
|