355 lines
11 KiB
Vue
355 lines
11 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="student-detail-container">
|
|||
|
|
|
|||
|
|
<view v-if="!loading && studentInfo" class="student-info-card">
|
|||
|
|
<view class="student-header">
|
|||
|
|
<view class="student-avatar-large">
|
|||
|
|
<text class="avatar-text">{{ (studentInfo.nickName || studentInfo.userName || 'S').charAt(0) }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="student-basic-info">
|
|||
|
|
<text class="student-name">{{ studentInfo.nickName || studentInfo.userName }}</text>
|
|||
|
|
<text class="student-id">学号:{{ studentInfo.userName }}</text>
|
|||
|
|
<text class="student-class" v-if="studentInfo.className">班级:{{ studentInfo.className }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 成绩统计 -->
|
|||
|
|
<view class="score-section">
|
|||
|
|
<view class="section-title">成绩统计</view>
|
|||
|
|
<view class="score-card">
|
|||
|
|
<view class="score-item">
|
|||
|
|
<text class="score-label">平均分</text>
|
|||
|
|
<text class="score-value">{{ averageScore || '--' }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="score-item">
|
|||
|
|
<text class="score-label">考试次数</text>
|
|||
|
|
<text class="score-value">{{ examCount || 0 }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 最近成绩 -->
|
|||
|
|
<view class="recent-scores-section">
|
|||
|
|
<view class="section-title">最近成绩</view>
|
|||
|
|
<view class="score-list">
|
|||
|
|
<view
|
|||
|
|
v-for="score in recentScores"
|
|||
|
|
:key="score.id"
|
|||
|
|
class="score-item-card"
|
|||
|
|
@click="goToScoreDetail(score)"
|
|||
|
|
>
|
|||
|
|
<view class="score-item-header">
|
|||
|
|
<text class="score-exam-name">{{ score.examName }}</text>
|
|||
|
|
<text class="score-value-text">{{ score.obtainedScore }}/{{ score.totalScore }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="score-item-info">
|
|||
|
|
<text class="score-time">{{ formatTime(score.submitTime || score.createTime) }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 学习记录 -->
|
|||
|
|
<view class="learning-section">
|
|||
|
|
<view class="section-title">学习记录</view>
|
|||
|
|
<view class="learning-card">
|
|||
|
|
<view class="learning-item">
|
|||
|
|
<text class="learning-label">学习时长</text>
|
|||
|
|
<text class="learning-value">{{ formatDuration(totalLearningTime) }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="learning-item">
|
|||
|
|
<text class="learning-label">学习课程数</text>
|
|||
|
|
<text class="learning-value">{{ courseCount || 0 }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import request from '@/utils/request.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
studentId: null,
|
|||
|
|
studentInfo: null,
|
|||
|
|
loading: false,
|
|||
|
|
recentScores: [],
|
|||
|
|
averageScore: null,
|
|||
|
|
examCount: 0,
|
|||
|
|
totalLearningTime: 0,
|
|||
|
|
courseCount: 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad(options) {
|
|||
|
|
if (options.id) {
|
|||
|
|
this.studentId = parseInt(options.id)
|
|||
|
|
this.loadStudentDetail()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
async loadStudentDetail() {
|
|||
|
|
this.loading = true
|
|||
|
|
try {
|
|||
|
|
// 获取学生基本信息(从用户列表接口获取)
|
|||
|
|
// 这里简化处理,实际应该调用获取单个用户信息的接口
|
|||
|
|
this.studentInfo = {
|
|||
|
|
userId: this.studentId,
|
|||
|
|
userName: 'student' + this.studentId,
|
|||
|
|
nickName: '学生' + this.studentId
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取学生成绩
|
|||
|
|
await this.loadStudentScores()
|
|||
|
|
|
|||
|
|
// 获取学生学习记录
|
|||
|
|
await this.loadLearningRecords()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载学生详情失败', error)
|
|||
|
|
uni.showToast({
|
|||
|
|
title: error.message || '加载失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
} finally {
|
|||
|
|
this.loading = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async loadStudentScores() {
|
|||
|
|
try {
|
|||
|
|
const response = await request.get('/study/score/list', {
|
|||
|
|
studentId: this.studentId
|
|||
|
|
})
|
|||
|
|
if (response.code === 200) {
|
|||
|
|
const scores = response.rows || response.data || []
|
|||
|
|
this.recentScores = scores.slice(0, 5)
|
|||
|
|
this.examCount = scores.length
|
|||
|
|
|
|||
|
|
// 计算平均分
|
|||
|
|
if (scores.length > 0) {
|
|||
|
|
const total = scores.reduce((sum, s) => sum + (parseFloat(s.obtainedScore) || 0), 0)
|
|||
|
|
this.averageScore = (total / scores.length).toFixed(1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载学生成绩失败', error)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async loadLearningRecords() {
|
|||
|
|
try {
|
|||
|
|
const response = await request.get(`/study/learningRecord/student/${this.studentId}`, {})
|
|||
|
|
if (response.code === 200) {
|
|||
|
|
const records = response.data || []
|
|||
|
|
this.courseCount = records.length
|
|||
|
|
// 计算总学习时长
|
|||
|
|
this.totalLearningTime = records.reduce((sum, r) => sum + (r.totalDuration || 0), 0)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载学习记录失败', error)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
goToScoreDetail(score) {
|
|||
|
|
uni.navigateTo({
|
|||
|
|
url: `/pages/score/detail?id=${score.id}${score.examId ? `&examId=${score.examId}` : ''}`
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatTime(timeStr) {
|
|||
|
|
if (!timeStr) return ''
|
|||
|
|
const date = new Date(timeStr)
|
|||
|
|
const year = date.getFullYear()
|
|||
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|||
|
|
const day = date.getDate().toString().padStart(2, '0')
|
|||
|
|
return `${year}-${month}-${day}`
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatDuration(seconds) {
|
|||
|
|
if (!seconds) return '0分钟'
|
|||
|
|
const hours = Math.floor(seconds / 3600)
|
|||
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|||
|
|
if (hours > 0) {
|
|||
|
|
return `${hours}小时${minutes}分钟`
|
|||
|
|
}
|
|||
|
|
return `${minutes}分钟`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.student-detail-container {
|
|||
|
|
padding: 30rpx;
|
|||
|
|
background-color: #f5f7fa;
|
|||
|
|
min-height: 100vh;
|
|||
|
|
|
|||
|
|
@media (min-width: 768px) {
|
|||
|
|
padding: 60rpx;
|
|||
|
|
max-width: 1200rpx;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.student-info-card {
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
padding: 40rpx;
|
|||
|
|
margin-bottom: 30rpx;
|
|||
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|||
|
|
|
|||
|
|
.student-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
.student-avatar-large {
|
|||
|
|
width: 120rpx;
|
|||
|
|
height: 120rpx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-right: 30rpx;
|
|||
|
|
|
|||
|
|
.avatar-text {
|
|||
|
|
font-size: 48rpx;
|
|||
|
|
color: #fff;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.student-basic-info {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 12rpx;
|
|||
|
|
|
|||
|
|
.student-name {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.student-id {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.student-class {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: rgb(55 140 224);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-section,
|
|||
|
|
.recent-scores-section,
|
|||
|
|
.learning-section {
|
|||
|
|
margin-bottom: 30rpx;
|
|||
|
|
|
|||
|
|
.section-title {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-card {
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 40rpx;
|
|||
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|||
|
|
|
|||
|
|
.score-item {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
.score-label {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #999;
|
|||
|
|
margin-bottom: 12rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-value {
|
|||
|
|
font-size: 48rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: rgb(55 140 224);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-list {
|
|||
|
|
.score-item-card {
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|||
|
|
|
|||
|
|
.score-item-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 16rpx;
|
|||
|
|
|
|||
|
|
.score-exam-name {
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-value-text {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: rgb(55 140 224);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-item-info {
|
|||
|
|
.score-time {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.learning-card {
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 40rpx;
|
|||
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|||
|
|
|
|||
|
|
.learning-item {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
.learning-label {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #999;
|
|||
|
|
margin-bottom: 12rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.learning-value {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: rgb(55 140 224);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|