- 将所有 error.response?.data?.message 改为 error.message - 影响所有教师端组件的错误处理 - 适配新的响应拦截器返回的错误对象结构 修改的文件: - CourseListView.vue - CourseDetailView.vue - PrepareModeView.vue - LessonListView.vue - LessonView.vue - LessonRecordsView.vue - SchoolCourseEditView.vue - ClassListView.vue - ClassStudentsView.vue - TaskListView.vue Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
474 lines
13 KiB
Vue
474 lines
13 KiB
Vue
<template>
|
|
<div class="class-students-view">
|
|
<div class="page-header">
|
|
<a-button type="text" @click="goBack">
|
|
<ArrowLeftOutlined />
|
|
</a-button>
|
|
<div class="header-info">
|
|
<h2><TeamOutlined /> 班级学生</h2>
|
|
<p class="page-desc" v-if="classInfo">{{ classInfo.name }} - 共 {{ students.length }} 名学生</p>
|
|
</div>
|
|
</div>
|
|
|
|
<a-spin :spinning="loading">
|
|
<!-- 统计卡片 -->
|
|
<div class="stats-cards" v-if="classInfo">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);">
|
|
<TeamOutlined />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-value">{{ classInfo.studentCount }}</div>
|
|
<div class="stat-label">学生总数</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
|
<BookOutlined />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-value">{{ classInfo.lessonCount }}</div>
|
|
<div class="stat-label">授课次数</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);">
|
|
<ReadOutlined />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-value">{{ totalReadingCount }}</div>
|
|
<div class="stat-label">阅读总次数</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 学生列表 -->
|
|
<div class="students-section">
|
|
<div class="section-header">
|
|
<h3>学生列表</h3>
|
|
<a-input-search
|
|
v-model:value="searchKeyword"
|
|
placeholder="搜索学生"
|
|
style="width: 200px;"
|
|
allow-clear
|
|
/>
|
|
</div>
|
|
|
|
<a-table
|
|
:columns="columns"
|
|
:data-source="filteredStudents"
|
|
:pagination="{ pageSize: 10 }"
|
|
row-key="id"
|
|
@row="handleRowClick"
|
|
>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'name'">
|
|
<div class="student-name-cell">
|
|
<a-avatar :size="32" :style="{ backgroundColor: getAvatarColor(record.id) }">
|
|
{{ record.name.charAt(0) }}
|
|
</a-avatar>
|
|
<span class="name">{{ record.name }}</span>
|
|
<span class="gender">{{ record.gender === '男' ? '👦' : record.gender === '女' ? '👧' : '' }}</span>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="column.key === 'readingCount'">
|
|
<a-tag color="blue">{{ record.readingCount || 0 }}</a-tag>
|
|
</template>
|
|
<template v-else-if="column.key === 'lessonCount'">
|
|
<a-tag color="orange">{{ record.lessonCount || 0 }}</a-tag>
|
|
</template>
|
|
<template v-else-if="column.key === 'avgScore'">
|
|
<a-progress
|
|
:percent="record.avgScore || 0"
|
|
:stroke-color="getScoreColor(record.avgScore)"
|
|
size="small"
|
|
style="width: 80px;"
|
|
/>
|
|
</template>
|
|
<template v-else-if="column.key === 'action'">
|
|
<a-button type="link" size="small" @click.stop="showStudentDetail(record)">
|
|
查看详情
|
|
</a-button>
|
|
</template>
|
|
</template>
|
|
</a-table>
|
|
</div>
|
|
</a-spin>
|
|
|
|
<!-- 学生详情抽屉 -->
|
|
<a-drawer
|
|
v-model:open="detailDrawerVisible"
|
|
title="学生详情"
|
|
placement="right"
|
|
:width="480"
|
|
>
|
|
<div class="student-detail" v-if="selectedStudent">
|
|
<div class="detail-header">
|
|
<a-avatar :size="64" :style="{ backgroundColor: getAvatarColor(selectedStudent.id) }">
|
|
{{ selectedStudent.name.charAt(0) }}
|
|
</a-avatar>
|
|
<div class="detail-info">
|
|
<h3>{{ selectedStudent.name }}</h3>
|
|
<div class="tags">
|
|
<a-tag v-if="selectedStudent.gender">{{ selectedStudent.gender }}</a-tag>
|
|
<a-tag color="blue">{{ selectedStudent.readingCount || 0 }} 次阅读</a-tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<a-descriptions :column="1" bordered class="detail-descriptions">
|
|
<a-descriptions-item label="班级">{{ classInfo?.name }}</a-descriptions-item>
|
|
<a-descriptions-item label="生日" v-if="selectedStudent.birthDate">
|
|
{{ formatDate(selectedStudent.birthDate) }}
|
|
</a-descriptions-item>
|
|
<a-descriptions-item label="家长姓名">{{ selectedStudent.parentName || '-' }}</a-descriptions-item>
|
|
<a-descriptions-item label="家长电话">{{ selectedStudent.parentPhone || '-' }}</a-descriptions-item>
|
|
<a-descriptions-item label="入学时间">{{ formatDate(selectedStudent.createdAt) }}</a-descriptions-item>
|
|
</a-descriptions>
|
|
|
|
<!-- 阅读统计 -->
|
|
<div class="detail-stats">
|
|
<h4>阅读统计</h4>
|
|
<div class="stats-grid">
|
|
<div class="stats-item">
|
|
<span class="value">{{ selectedStudent.lessonCount || 0 }}</span>
|
|
<span class="label">上课次数</span>
|
|
</div>
|
|
<div class="stats-item">
|
|
<span class="value">{{ selectedStudent.readingCount || 0 }}</span>
|
|
<span class="label">阅读次数</span>
|
|
</div>
|
|
<div class="stats-item">
|
|
<span class="value">{{ selectedStudent.avgScore || 0 }}%</span>
|
|
<span class="label">平均得分</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 最近阅读记录 -->
|
|
<div class="recent-records">
|
|
<h4>最近阅读记录</h4>
|
|
<div class="records-list" v-if="recentRecords.length > 0">
|
|
<div v-for="record in recentRecords" :key="record.id" class="record-item">
|
|
<div class="record-course">{{ record.lesson?.course?.name || '未知课程' }}</div>
|
|
<div class="record-date">{{ formatDateTime(record.lesson?.startDatetime) }}</div>
|
|
<div class="record-score">
|
|
<a-rate :value="(record.focus || 0 + record.participation || 0) / 2" disabled allow-half style="font-size: 12px;" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="empty-records" v-else>
|
|
<p>暂无阅读记录</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a-drawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
import { message } from 'ant-design-vue';
|
|
import {
|
|
ArrowLeftOutlined,
|
|
TeamOutlined,
|
|
BookOutlined,
|
|
ReadOutlined,
|
|
} from '@ant-design/icons-vue';
|
|
import { getTeacherClassStudents, getStudentRecords } from '@/api/teacher';
|
|
import type { Student } from '@/api/school';
|
|
import dayjs from 'dayjs';
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const classId = computed(() => Number(route.params.id));
|
|
|
|
const loading = ref(false);
|
|
const classInfo = ref<ClassInfo | null>(null);
|
|
const students = ref<Student[]>([]);
|
|
const searchKeyword = ref('');
|
|
const detailDrawerVisible = ref(false);
|
|
const selectedStudent = ref<Student | null>(null);
|
|
const recentRecords = ref<any[]>([]);
|
|
|
|
const avatarColors = ['#FF8C42', '#667eea', '#f093fb', '#4facfe', '#43e97b', '#faad14', '#eb2f96'];
|
|
const getAvatarColor = (id: number) => avatarColors[id % avatarColors.length];
|
|
|
|
const columns = [
|
|
{ title: '姓名', key: 'name', dataIndex: 'name' },
|
|
{ title: '阅读次数', key: 'readingCount', dataIndex: 'readingCount', width: 100 },
|
|
{ title: '上课次数', key: 'lessonCount', dataIndex: 'lessonCount', width: 100 },
|
|
{ title: '平均得分', key: 'avgScore', width: 120 },
|
|
{ title: '操作', key: 'action', width: 100 },
|
|
];
|
|
|
|
const filteredStudents = computed(() => {
|
|
if (!searchKeyword.value) return students.value;
|
|
const keyword = searchKeyword.value.toLowerCase();
|
|
return students.value.filter(s => s.name.toLowerCase().includes(keyword));
|
|
});
|
|
|
|
const totalReadingCount = computed(() => {
|
|
return students.value.reduce((sum, s) => sum + (s.readingCount || 0), 0);
|
|
});
|
|
|
|
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD');
|
|
const formatDateTime = (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm');
|
|
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 80) return '#52c41a';
|
|
if (score >= 60) return '#faad14';
|
|
return '#ff4d4f';
|
|
};
|
|
|
|
const goBack = () => router.back();
|
|
|
|
const handleRowClick = (record: Student) => {
|
|
showStudentDetail(record);
|
|
};
|
|
|
|
const showStudentDetail = async (student: Student) => {
|
|
selectedStudent.value = student;
|
|
detailDrawerVisible.value = true;
|
|
|
|
// 加载最近阅读记录
|
|
try {
|
|
// 这里应该调用获取学生阅读记录的API
|
|
recentRecords.value = [];
|
|
} catch (error) {
|
|
console.error('Failed to load student records:', error);
|
|
}
|
|
};
|
|
|
|
const loadStudents = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const data = await getTeacherClassStudents(classId.value);
|
|
classInfo.value = data.class ? {
|
|
id: data.class.id,
|
|
name: data.class.name,
|
|
grade: data.class.grade,
|
|
studentCount: data.class.studentCount,
|
|
lessonCount: data.class.lessonCount,
|
|
} as any : null;
|
|
students.value = data.items.map((s: any) => ({
|
|
...s,
|
|
avgScore: Math.round(Math.random() * 40 + 60), // 临时模拟数据
|
|
}));
|
|
} catch (error: any) {
|
|
message.error(error.message || '加载失败');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadStudents();
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
$primary-color: #FF8C42;
|
|
$primary-light: #FFF4EC;
|
|
|
|
.class-students-view {
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 24px;
|
|
|
|
.header-info {
|
|
h2 {
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.page-desc {
|
|
margin: 4px 0 0;
|
|
color: #999;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.stats-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
|
|
@media (max-width: 768px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stat-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 20px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
|
|
.stat-icon {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
color: white;
|
|
}
|
|
|
|
.stat-content {
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.students-section {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
|
|
h3 {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
.student-name-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
|
|
.name {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.gender {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 学生详情抽屉
|
|
.student-detail {
|
|
.detail-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
|
|
.detail-info {
|
|
h3 {
|
|
margin: 0 0 8px;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.tags {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.detail-descriptions {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.detail-stats {
|
|
margin-bottom: 24px;
|
|
|
|
h4 {
|
|
margin: 0 0 16px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
|
|
.stats-item {
|
|
text-align: center;
|
|
padding: 16px;
|
|
background: #fafafa;
|
|
border-radius: 8px;
|
|
|
|
.value {
|
|
display: block;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: $primary-color;
|
|
}
|
|
|
|
.label {
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.recent-records {
|
|
h4 {
|
|
margin: 0 0 16px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.records-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.record-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px;
|
|
background: #fafafa;
|
|
border-radius: 8px;
|
|
|
|
.record-course {
|
|
flex: 1;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.record-date {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-right: 12px;
|
|
}
|
|
}
|
|
|
|
.empty-records {
|
|
text-align: center;
|
|
padding: 24px;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
</style>
|