hookehuyr

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

添加学生列表API接口并集成到班级页面
实现学生列表的分页加载、搜索和筛选功能
优化列表加载性能,添加请求取消控制
1 /* 1 /*
2 * @Date: 2025-06-23 11:46:21 2 * @Date: 2025-06-23 11:46:21
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-06-25 15:48:41 4 + * @LastEditTime: 2025-06-26 10:35:45
5 * @FilePath: /mlaj/src/api/teacher.js 5 * @FilePath: /mlaj/src/api/teacher.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -11,6 +11,7 @@ const Api = { ...@@ -11,6 +11,7 @@ const Api = {
11 TEACHER_GRADE_CLASS_LIST: '/srv/?a=user&t=teacher_grade_class_group_list', 11 TEACHER_GRADE_CLASS_LIST: '/srv/?a=user&t=teacher_grade_class_group_list',
12 TEACHER_FIND_SETTINGS: '/srv/?a=task&t=teacher_find_settings', 12 TEACHER_FIND_SETTINGS: '/srv/?a=task&t=teacher_find_settings',
13 TEACHER_ADD_TASK: '/srv/?a=task&t=teacher_add', 13 TEACHER_ADD_TASK: '/srv/?a=task&t=teacher_add',
14 + STUDENT_LIST: '/srv/?a=user&t=student_list',
14 } 15 }
15 16
16 /** 17 /**
...@@ -43,3 +44,14 @@ export const getTeacherFindSettingsAPI = (params) => fn(fetch.get(Api.TEACHER_FI ...@@ -43,3 +44,14 @@ export const getTeacherFindSettingsAPI = (params) => fn(fetch.get(Api.TEACHER_FI
43 * @returns {Object} data { id } 44 * @returns {Object} data { id }
44 */ 45 */
45 export const setTeacherTaskAPI = (params) => fn(fetch.post(Api.TEACHER_ADD_TASK, params)) 46 export const setTeacherTaskAPI = (params) => fn(fetch.post(Api.TEACHER_ADD_TASK, params))
47 +
48 +/**
49 + * 获取学员列表
50 + * @param {*} grade_id 年级ID
51 + * @param {*} class_id 班级ID
52 + * @param {*} keyword 搜索
53 + * @param {*} limit
54 + * @param {*} page
55 + * @returns {Object} data { count, user_list[{id, name, avatar, mobile, class_list[{id, class_name}], last_checkin_time, last_checkin_time_desc}] }
56 + */
57 +export const getStudentListAPI = (params) => fn(fetch.get(Api.STUDENT_LIST, params))
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2025-01-20 10:00:00 3 * @Date: 2025-01-20 10:00:00
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2025-06-25 17:22:37 5 + * @LastEditTime: 2025-06-26 11:06:33
6 * @FilePath: /mlaj/src/views/teacher/myClassPage.vue 6 * @FilePath: /mlaj/src/views/teacher/myClassPage.vue
7 * @Description: 我的班级页面 7 * @Description: 我的班级页面
8 --> 8 -->
...@@ -82,19 +82,19 @@ ...@@ -82,19 +82,19 @@
82 <!-- 标题和搜索 --> 82 <!-- 标题和搜索 -->
83 <div class="p-4 border-b border-gray-100 bg-white"> 83 <div class="p-4 border-b border-gray-100 bg-white">
84 <div class="flex items-center justify-between mb-3"> 84 <div class="flex items-center justify-between mb-3">
85 - <h3 class="text-lg font-bold text-gray-800">班级成员 ({{ studentList.length }})</h3> 85 + <h3 class="text-lg font-bold text-gray-800">班级成员 ({{ studentCount }})</h3>
86 <div @click="showSortPopup = true" class="flex items-center text-sm text-gray-600 cursor-pointer"> 86 <div @click="showSortPopup = true" class="flex items-center text-sm text-gray-600 cursor-pointer">
87 <span>{{ sortFilter }}</span> 87 <span>{{ sortFilter }}</span>
88 <van-icon name="arrow-down" size="14" class="ml-1" /> 88 <van-icon name="arrow-down" size="14" class="ml-1" />
89 </div> 89 </div>
90 </div> 90 </div>
91 - <van-search v-model="searchKeyword" placeholder="请搜索" shape="round" @search="handleSearch" @input="handleSearch" /> 91 + <van-search v-model="searchKeyword" placeholder="请搜索" shape="round" @search="handleSearch" @input="handleSearchInput" />
92 </div> 92 </div>
93 93
94 <!-- 学生列表 --> 94 <!-- 学生列表 -->
95 <div class="p-4"> 95 <div class="p-4">
96 <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> 96 <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
97 - <div v-for="student in filteredStudentList" :key="student.id" 97 + <div v-for="student in studentList" :key="student.id"
98 class="flex items-center justify-between py-3 border-gray-50 bg-white rounded-xl p-4 text-center shadow-sm mb-4" 98 class="flex items-center justify-between py-3 border-gray-50 bg-white rounded-xl p-4 text-center shadow-sm mb-4"
99 @click="handleStudentClick(student)"> 99 @click="handleStudentClick(student)">
100 <div class="flex items-center flex-1"> 100 <div class="flex items-center flex-1">
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
107 <font-awesome-icon v-else icon="mars" color="#ec4899" class="mr-2" style="font-size: 0.85rem;" /> 107 <font-awesome-icon v-else icon="mars" color="#ec4899" class="mr-2" style="font-size: 0.85rem;" />
108 </div> 108 </div>
109 <div class="text-sm text-gray-500" style="text-align: left;"> 109 <div class="text-sm text-gray-500" style="text-align: left;">
110 - <span v-for="(item, index) in student.class_list" :key="index" class="mr-2">{{ item }}</span> 110 + <span v-for="(item, index) in student.class_list" :key="index" class="mr-2">{{ item.class_name }}</span>
111 </div> 111 </div>
112 </div> 112 </div>
113 </div> 113 </div>
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
116 <van-icon name="phone-o" size="12" class="mr-1" /> 116 <van-icon name="phone-o" size="12" class="mr-1" />
117 <span>{{ formatPhone(student.mobile) }}</span> 117 <span>{{ formatPhone(student.mobile) }}</span>
118 </div> 118 </div>
119 - <div class="text-xs text-gray-400">{{ student.last_checkin_time }}</div> 119 + <div class="text-xs text-gray-400">{{ student.last_checkin_time_desc }}</div>
120 </div> 120 </div>
121 <van-icon name="arrow" color="#d1d5db" size="16" class="ml-2" /> 121 <van-icon name="arrow" color="#d1d5db" size="16" class="ml-2" />
122 </div> 122 </div>
...@@ -154,13 +154,13 @@ ...@@ -154,13 +154,13 @@
154 </template> 154 </template>
155 155
156 <script setup> 156 <script setup>
157 -import { ref, computed, onMounted } from 'vue' 157 +import { ref, computed, onMounted, onUnmounted } from 'vue'
158 import { useRouter } from 'vue-router' 158 import { useRouter } from 'vue-router'
159 import AppLayout from '@/layouts/AppLayout.vue' 159 import AppLayout from '@/layouts/AppLayout.vue'
160 import { useTitle } from '@vueuse/core'; 160 import { useTitle } from '@vueuse/core';
161 import { useAuth } from '@/contexts/auth' 161 import { useAuth } from '@/contexts/auth'
162 162
163 -import { getTeacherGradeClassListAPI } from "@/api/teacher"; 163 +import { getTeacherGradeClassListAPI, getStudentListAPI } from "@/api/teacher";
164 164
165 const router = useRouter() 165 const router = useRouter()
166 const route = useRoute() 166 const route = useRoute()
...@@ -197,105 +197,18 @@ const taskCompletionRate = ref(76) ...@@ -197,105 +197,18 @@ const taskCompletionRate = ref(76)
197 const learningProgress = ref(92) 197 const learningProgress = ref(92)
198 198
199 // 学生列表 199 // 学生列表
200 -const studentList = ref([ 200 +const studentList = ref([])
201 - { 201 +
202 - id: 1, 202 +const studentCount = ref(0);
203 - name: '张明',
204 - gender: 'male',
205 - class_list: ['高一(3)班', '高二(1)班', '高二(2)班'],
206 - mobile: '13812345678',
207 - last_checkin_time: '5分钟前活跃',
208 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
209 - },
210 - {
211 - id: 2,
212 - name: '李华',
213 - gender: 'female',
214 - class_list: ['高一(3)班', '高二(1)班', '高二(2)班'],
215 - mobile: '13987654321',
216 - last_checkin_time: '10分钟前活跃',
217 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
218 - },
219 - {
220 - id: 3,
221 - name: '王强',
222 - gender: 'male',
223 - class_list: ['高一(2)班', '高二(1)班', '高二(2)班'],
224 - mobile: '13512349876',
225 - last_checkin_time: '15分钟前活跃',
226 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
227 - },
228 - {
229 - id: 4,
230 - name: '赵敏',
231 - gender: 'female',
232 - class_list: ['高一(1)班', '高二(1)班', '高二(2)班'],
233 - mobile: '13643214321',
234 - last_checkin_time: '30分钟前活跃',
235 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
236 - },
237 - {
238 - id: 1,
239 - name: '张明',
240 - gender: 'male',
241 - class_list: ['高一(3)班', '高二(1)班', '高二(2)班'],
242 - mobile: '13812345678',
243 - last_checkin_time: '5分钟前活跃',
244 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
245 - },
246 - {
247 - id: 2,
248 - name: '李华',
249 - gender: 'female',
250 - class_list: ['高一(3)班'],
251 - mobile: '13987654321',
252 - last_checkin_time: '10分钟前活跃',
253 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
254 - },
255 - {
256 - id: 3,
257 - name: '王强',
258 - gender: 'male',
259 - class_list: ['高一(2)班'],
260 - mobile: '13512349876',
261 - last_checkin_time: '15分钟前活跃',
262 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
263 - },
264 - {
265 - id: 4,
266 - name: '赵敏',
267 - gender: 'female',
268 - class_list: ['高一(1)班'],
269 - mobile: '13643214321',
270 - last_checkin_time: '30分钟前活跃',
271 - avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
272 - }
273 -])
274 203
275 // 列表加载状态 204 // 列表加载状态
276 const loading = ref(false) 205 const loading = ref(false)
277 const finished = ref(false) 206 const finished = ref(false)
207 +const limit = ref(3)
208 +const page = ref(0)
278 209
279 -/** 210 +// 请求控制器,用于取消重复请求
280 - * 过滤后的学生列表 211 +let abortController = null;
281 - */
282 -const filteredStudentList = computed(() => {
283 - let filtered = studentList.value
284 -
285 - // 按搜索关键词筛选
286 - if (searchKeyword.value) {
287 - filtered = filtered.filter(student =>
288 - student.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
289 - )
290 - }
291 -
292 - // 排序
293 - if (sortFilter.value === '按姓名') {
294 - filtered.sort((a, b) => a.name.localeCompare(b.name))
295 - }
296 -
297 - return filtered
298 -})
299 212
300 /** 213 /**
301 * 格式化手机号 214 * 格式化手机号
...@@ -308,52 +221,61 @@ const formatPhone = (phone) => { ...@@ -308,52 +221,61 @@ const formatPhone = (phone) => {
308 } 221 }
309 222
310 /** 223 /**
224 + * 重置分页参数并重新加载数据
225 + */
226 +const resetAndReload = () => {
227 + page.value = 0;
228 + studentList.value = [];
229 + finished.value = false;
230 + loading.value = true;
231 + onLoad();
232 +}
233 +
234 +/**
311 * 处理年级筛选变化 235 * 处理年级筛选变化
312 - * @param {string} value - 选中的年级 236 + * @param {string} val - 选中的年级
313 */ 237 */
314 const handleGradeChange = async (val) => { 238 const handleGradeChange = async (val) => {
315 console.log('val', val); 239 console.log('val', val);
316 selectGradeValue.value = val; 240 selectGradeValue.value = val;
241 + // 重置班级和课程选择
242 + selectClassValue.value = null;
243 + selectCourseValue.value = null;
244 +
317 // 根据年级ID 更新过滤列表 245 // 根据年级ID 更新过滤列表
318 - getFilterList(val); 246 + await getFilterList(val);
319 - // 重置分页参数 247 +
320 - // page.value = 0 248 + // 重置分页参数并重新加载数据
321 - // checkinDataList.value = [] 249 + resetAndReload();
322 - // finished.value = false
323 - // // 重新加载数据
324 - // onLoad()
325 } 250 }
326 251
327 /** 252 /**
328 * 处理班级筛选变化 253 * 处理班级筛选变化
329 - * @param {string} value - 选中的班级 254 + * @param {string} val - 选中的班级
330 */ 255 */
331 -const handleClassChange = (val) => { 256 +const handleClassChange = async (val) => {
332 console.log('val', val); 257 console.log('val', val);
333 selectClassValue.value = val; 258 selectClassValue.value = val;
259 + // 重置课程选择
260 + selectCourseValue.value = null;
261 +
334 // 根据年级ID和班级ID 更新过滤列表 262 // 根据年级ID和班级ID 更新过滤列表
335 - getFilterList(selectGradeValue.value, val); 263 + await getFilterList(selectGradeValue.value, val);
336 - // 重置分页参数 264 +
337 - // page.value = 0 265 + // 重置分页参数并重新加载数据
338 - // checkinDataList.value = [] 266 + resetAndReload();
339 - // finished.value = false
340 - // // 重新加载数据
341 - // onLoad()
342 } 267 }
343 268
344 /** 269 /**
345 * 处理课程筛选变化 270 * 处理课程筛选变化
346 - * @param {string} value - 选中的课程 271 + * @param {string} val - 选中的课程
347 */ 272 */
348 const handleCourseChange = (val) => { 273 const handleCourseChange = (val) => {
349 console.log('val', val); 274 console.log('val', val);
350 selectCourseValue.value = val; 275 selectCourseValue.value = val;
351 - // 重置分页参数 276 +
352 - // page.value = 0 277 + // 重置分页参数并重新加载数据
353 - // checkinDataList.value = [] 278 + resetAndReload();
354 - // finished.value = false
355 - // // 重新加载数据
356 - // onLoad()
357 } 279 }
358 280
359 /** 281 /**
...@@ -374,12 +296,33 @@ const onSortSelect = (option) => { ...@@ -374,12 +296,33 @@ const onSortSelect = (option) => {
374 handleSortChange(option.value) 296 handleSortChange(option.value)
375 } 297 }
376 298
299 +// 搜索防抖定时器
300 +let searchTimer = null;
301 +
302 +/**
303 + * 处理搜索输入 - 只更新显示值,不触发搜索
304 + * @param {string} value - 搜索关键词
305 + */
306 +const handleSearchInput = (value) => {
307 + // 只更新输入框显示,不触发搜索
308 + // searchKeyword 通过 v-model 自动更新
309 +}
310 +
377 /** 311 /**
378 - * 处理搜索 312 + * 处理搜索 - 立即搜索,无需防抖
379 * @param {string} value - 搜索关键词 313 * @param {string} value - 搜索关键词
380 */ 314 */
381 const handleSearch = (value) => { 315 const handleSearch = (value) => {
382 - console.log('搜索:', value) 316 + console.log('搜索:', value);
317 +
318 + // 清除之前的定时器
319 + if (searchTimer) {
320 + clearTimeout(searchTimer);
321 + }
322 +
323 + // 立即执行搜索
324 + searchKeyword.value = value;
325 + resetAndReload();
383 } 326 }
384 327
385 /** 328 /**
...@@ -397,11 +340,42 @@ const handleStudentClick = (student) => { ...@@ -397,11 +340,42 @@ const handleStudentClick = (student) => {
397 /** 340 /**
398 * 加载更多数据 341 * 加载更多数据
399 */ 342 */
400 -const onLoad = () => { 343 +const onLoad = async () => {
401 - setTimeout(() => { 344 + // 取消之前的请求
402 - loading.value = false 345 + if (abortController) {
403 - finished.value = true 346 + abortController.abort();
404 - }, 1000) 347 + }
348 +
349 + // 创建新的请求控制器
350 + abortController = new AbortController();
351 +
352 + const nextPage = page.value;
353 +
354 + try {
355 + const res = await getStudentListAPI({
356 + limit: limit.value,
357 + page: nextPage,
358 + grade_id: selectGradeValue.value,
359 + class_id: selectClassValue.value,
360 + course_id: selectCourseValue.value,
361 + keyword: searchKeyword.value,
362 + });
363 +
364 + if (res.code) {
365 + // 整理数据结构
366 + studentList.value = [...studentList.value, ...res.data.user_list];
367 + finished.value = res.data.user_list.length < limit.value;
368 + page.value = nextPage + 1;
369 + studentCount.value = res.data.count;
370 + }
371 + } catch (error) {
372 + // 如果是取消请求的错误,不需要处理
373 + if (error.name !== 'AbortError') {
374 + console.error('加载学生列表失败:', error);
375 + }
376 + } finally {
377 + loading.value = false;
378 + }
405 } 379 }
406 380
407 const gradeList = ref([]); 381 const gradeList = ref([]);
...@@ -449,11 +423,26 @@ const getFilterList = async (grade_id=null, class_id=null) => { ...@@ -449,11 +423,26 @@ const getFilterList = async (grade_id=null, class_id=null) => {
449 * 组件挂载时初始化数据 423 * 组件挂载时初始化数据
450 */ 424 */
451 onMounted(async () => { 425 onMounted(async () => {
452 - // 这里可以调用API获取实际数据
453 - console.log('我的班级页面已加载')
454 // 获取老师的年级、班级、课程列表信息 426 // 获取老师的年级、班级、课程列表信息
455 getFilterList(); 427 getFilterList();
456 }) 428 })
429 +
430 +/**
431 + * 组件卸载时清理资源
432 + */
433 +onUnmounted(() => {
434 + // 清理搜索防抖定时器
435 + if (searchTimer) {
436 + clearTimeout(searchTimer);
437 + searchTimer = null;
438 + }
439 +
440 + // 取消进行中的请求
441 + if (abortController) {
442 + abortController.abort();
443 + abortController = null;
444 + }
445 +})
457 </script> 446 </script>
458 447
459 <style lang="less"> 448 <style lang="less">
......