hookehuyr

feat(teacher): 实现班级学生列表接口集成和分页功能

添加学生列表API接口并集成到班级页面
实现学生列表的分页加载、搜索和筛选功能
优化列表加载性能,添加请求取消控制
/*
* @Date: 2025-06-23 11:46:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-25 15:48:41
* @LastEditTime: 2025-06-26 10:35:45
* @FilePath: /mlaj/src/api/teacher.js
* @Description: 文件描述
*/
......@@ -11,6 +11,7 @@ const Api = {
TEACHER_GRADE_CLASS_LIST: '/srv/?a=user&t=teacher_grade_class_group_list',
TEACHER_FIND_SETTINGS: '/srv/?a=task&t=teacher_find_settings',
TEACHER_ADD_TASK: '/srv/?a=task&t=teacher_add',
STUDENT_LIST: '/srv/?a=user&t=student_list',
}
/**
......@@ -43,3 +44,14 @@ export const getTeacherFindSettingsAPI = (params) => fn(fetch.get(Api.TEACHER_FI
* @returns {Object} data { id }
*/
export const setTeacherTaskAPI = (params) => fn(fetch.post(Api.TEACHER_ADD_TASK, params))
/**
* 获取学员列表
* @param {*} grade_id 年级ID
* @param {*} class_id 班级ID
* @param {*} keyword 搜索
* @param {*} limit
* @param {*} page
* @returns {Object} data { count, user_list[{id, name, avatar, mobile, class_list[{id, class_name}], last_checkin_time, last_checkin_time_desc}] }
*/
export const getStudentListAPI = (params) => fn(fetch.get(Api.STUDENT_LIST, params))
......
......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-01-20 10:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-25 17:22:37
* @LastEditTime: 2025-06-26 11:06:33
* @FilePath: /mlaj/src/views/teacher/myClassPage.vue
* @Description: 我的班级页面
-->
......@@ -82,19 +82,19 @@
<!-- 标题和搜索 -->
<div class="p-4 border-b border-gray-100 bg-white">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-bold text-gray-800">班级成员 ({{ studentList.length }})</h3>
<h3 class="text-lg font-bold text-gray-800">班级成员 ({{ studentCount }})</h3>
<div @click="showSortPopup = true" class="flex items-center text-sm text-gray-600 cursor-pointer">
<span>{{ sortFilter }}</span>
<van-icon name="arrow-down" size="14" class="ml-1" />
</div>
</div>
<van-search v-model="searchKeyword" placeholder="请搜索" shape="round" @search="handleSearch" @input="handleSearch" />
<van-search v-model="searchKeyword" placeholder="请搜索" shape="round" @search="handleSearch" @input="handleSearchInput" />
</div>
<!-- 学生列表 -->
<div class="p-4">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="student in filteredStudentList" :key="student.id"
<div v-for="student in studentList" :key="student.id"
class="flex items-center justify-between py-3 border-gray-50 bg-white rounded-xl p-4 text-center shadow-sm mb-4"
@click="handleStudentClick(student)">
<div class="flex items-center flex-1">
......@@ -107,7 +107,7 @@
<font-awesome-icon v-else icon="mars" color="#ec4899" class="mr-2" style="font-size: 0.85rem;" />
</div>
<div class="text-sm text-gray-500" style="text-align: left;">
<span v-for="(item, index) in student.class_list" :key="index" class="mr-2">{{ item }}</span>
<span v-for="(item, index) in student.class_list" :key="index" class="mr-2">{{ item.class_name }}</span>
</div>
</div>
</div>
......@@ -116,7 +116,7 @@
<van-icon name="phone-o" size="12" class="mr-1" />
<span>{{ formatPhone(student.mobile) }}</span>
</div>
<div class="text-xs text-gray-400">{{ student.last_checkin_time }}</div>
<div class="text-xs text-gray-400">{{ student.last_checkin_time_desc }}</div>
</div>
<van-icon name="arrow" color="#d1d5db" size="16" class="ml-2" />
</div>
......@@ -154,13 +154,13 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import AppLayout from '@/layouts/AppLayout.vue'
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth'
import { getTeacherGradeClassListAPI } from "@/api/teacher";
import { getTeacherGradeClassListAPI, getStudentListAPI } from "@/api/teacher";
const router = useRouter()
const route = useRoute()
......@@ -197,105 +197,18 @@ const taskCompletionRate = ref(76)
const learningProgress = ref(92)
// 学生列表
const studentList = ref([
{
id: 1,
name: '张明',
gender: 'male',
class_list: ['高一(3)班', '高二(1)班', '高二(2)班'],
mobile: '13812345678',
last_checkin_time: '5分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 2,
name: '李华',
gender: 'female',
class_list: ['高一(3)班', '高二(1)班', '高二(2)班'],
mobile: '13987654321',
last_checkin_time: '10分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 3,
name: '王强',
gender: 'male',
class_list: ['高一(2)班', '高二(1)班', '高二(2)班'],
mobile: '13512349876',
last_checkin_time: '15分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 4,
name: '赵敏',
gender: 'female',
class_list: ['高一(1)班', '高二(1)班', '高二(2)班'],
mobile: '13643214321',
last_checkin_time: '30分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 1,
name: '张明',
gender: 'male',
class_list: ['高一(3)班', '高二(1)班', '高二(2)班'],
mobile: '13812345678',
last_checkin_time: '5分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 2,
name: '李华',
gender: 'female',
class_list: ['高一(3)班'],
mobile: '13987654321',
last_checkin_time: '10分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 3,
name: '王强',
gender: 'male',
class_list: ['高一(2)班'],
mobile: '13512349876',
last_checkin_time: '15分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 4,
name: '赵敏',
gender: 'female',
class_list: ['高一(1)班'],
mobile: '13643214321',
last_checkin_time: '30分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
}
])
const studentList = ref([])
const studentCount = ref(0);
// 列表加载状态
const loading = ref(false)
const finished = ref(false)
const limit = ref(3)
const page = ref(0)
/**
* 过滤后的学生列表
*/
const filteredStudentList = computed(() => {
let filtered = studentList.value
// 按搜索关键词筛选
if (searchKeyword.value) {
filtered = filtered.filter(student =>
student.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
// 排序
if (sortFilter.value === '按姓名') {
filtered.sort((a, b) => a.name.localeCompare(b.name))
}
return filtered
})
// 请求控制器,用于取消重复请求
let abortController = null;
/**
* 格式化手机号
......@@ -308,52 +221,61 @@ const formatPhone = (phone) => {
}
/**
* 重置分页参数并重新加载数据
*/
const resetAndReload = () => {
page.value = 0;
studentList.value = [];
finished.value = false;
loading.value = true;
onLoad();
}
/**
* 处理年级筛选变化
* @param {string} value - 选中的年级
* @param {string} val - 选中的年级
*/
const handleGradeChange = async (val) => {
console.log('val', val);
selectGradeValue.value = val;
// 重置班级和课程选择
selectClassValue.value = null;
selectCourseValue.value = null;
// 根据年级ID 更新过滤列表
getFilterList(val);
// 重置分页参数
// page.value = 0
// checkinDataList.value = []
// finished.value = false
// // 重新加载数据
// onLoad()
await getFilterList(val);
// 重置分页参数并重新加载数据
resetAndReload();
}
/**
* 处理班级筛选变化
* @param {string} value - 选中的班级
* @param {string} val - 选中的班级
*/
const handleClassChange = (val) => {
const handleClassChange = async (val) => {
console.log('val', val);
selectClassValue.value = val;
// 重置课程选择
selectCourseValue.value = null;
// 根据年级ID和班级ID 更新过滤列表
getFilterList(selectGradeValue.value, val);
// 重置分页参数
// page.value = 0
// checkinDataList.value = []
// finished.value = false
// // 重新加载数据
// onLoad()
await getFilterList(selectGradeValue.value, val);
// 重置分页参数并重新加载数据
resetAndReload();
}
/**
* 处理课程筛选变化
* @param {string} value - 选中的课程
* @param {string} val - 选中的课程
*/
const handleCourseChange = (val) => {
console.log('val', val);
selectCourseValue.value = val;
// 重置分页参数
// page.value = 0
// checkinDataList.value = []
// finished.value = false
// // 重新加载数据
// onLoad()
// 重置分页参数并重新加载数据
resetAndReload();
}
/**
......@@ -374,12 +296,33 @@ const onSortSelect = (option) => {
handleSortChange(option.value)
}
// 搜索防抖定时器
let searchTimer = null;
/**
* 处理搜索输入 - 只更新显示值,不触发搜索
* @param {string} value - 搜索关键词
*/
const handleSearchInput = (value) => {
// 只更新输入框显示,不触发搜索
// searchKeyword 通过 v-model 自动更新
}
/**
* 处理搜索
* 处理搜索 - 立即搜索,无需防抖
* @param {string} value - 搜索关键词
*/
const handleSearch = (value) => {
console.log('搜索:', value)
console.log('搜索:', value);
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer);
}
// 立即执行搜索
searchKeyword.value = value;
resetAndReload();
}
/**
......@@ -397,11 +340,42 @@ const handleStudentClick = (student) => {
/**
* 加载更多数据
*/
const onLoad = () => {
setTimeout(() => {
loading.value = false
finished.value = true
}, 1000)
const onLoad = async () => {
// 取消之前的请求
if (abortController) {
abortController.abort();
}
// 创建新的请求控制器
abortController = new AbortController();
const nextPage = page.value;
try {
const res = await getStudentListAPI({
limit: limit.value,
page: nextPage,
grade_id: selectGradeValue.value,
class_id: selectClassValue.value,
course_id: selectCourseValue.value,
keyword: searchKeyword.value,
});
if (res.code) {
// 整理数据结构
studentList.value = [...studentList.value, ...res.data.user_list];
finished.value = res.data.user_list.length < limit.value;
page.value = nextPage + 1;
studentCount.value = res.data.count;
}
} catch (error) {
// 如果是取消请求的错误,不需要处理
if (error.name !== 'AbortError') {
console.error('加载学生列表失败:', error);
}
} finally {
loading.value = false;
}
}
const gradeList = ref([]);
......@@ -449,11 +423,26 @@ const getFilterList = async (grade_id=null, class_id=null) => {
* 组件挂载时初始化数据
*/
onMounted(async () => {
// 这里可以调用API获取实际数据
console.log('我的班级页面已加载')
// 获取老师的年级、班级、课程列表信息
getFilterList();
})
/**
* 组件卸载时清理资源
*/
onUnmounted(() => {
// 清理搜索防抖定时器
if (searchTimer) {
clearTimeout(searchTimer);
searchTimer = null;
}
// 取消进行中的请求
if (abortController) {
abortController.abort();
abortController = null;
}
})
</script>
<style lang="less">
......