2025-12-03 18:58:36 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="student-list-container">
|
|
|
|
|
|
<!-- 顶部导航栏 -->
|
|
|
|
|
|
<custom-navbar title="学生管理"></custom-navbar>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 搜索栏 -->
|
|
|
|
|
|
<view class="search-section">
|
|
|
|
|
|
<view class="search-box">
|
2025-12-10 13:15:26 +08:00
|
|
|
|
<text class="search-icon">🔍</text>
|
2025-12-03 18:58:36 +08:00
|
|
|
|
<u-input
|
|
|
|
|
|
v-model="searchKeyword"
|
|
|
|
|
|
placeholder="搜索学生姓名、学号..."
|
|
|
|
|
|
:clearable="true"
|
|
|
|
|
|
@input="handleSearch"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 班级筛选 -->
|
|
|
|
|
|
<view class="filter-section">
|
|
|
|
|
|
<view class="filter-item" @click="showClassPicker = true">
|
|
|
|
|
|
<text class="filter-label">班级:</text>
|
|
|
|
|
|
<text class="filter-value">{{ selectedClassName || '全部班级' }}</text>
|
|
|
|
|
|
<text class="filter-icon">▼</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 班级选择器 -->
|
|
|
|
|
|
<u-picker
|
|
|
|
|
|
:show="showClassPicker"
|
|
|
|
|
|
:columns="[classOptions]"
|
|
|
|
|
|
keyName="label"
|
|
|
|
|
|
@confirm="onClassConfirm"
|
|
|
|
|
|
@cancel="showClassPicker = false"
|
|
|
|
|
|
></u-picker>
|
|
|
|
|
|
|
|
|
|
|
|
<u-empty v-if="filteredStudentList.length === 0 && !loading" mode="data" text="暂无学生"></u-empty>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-else class="student-list">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="student in filteredStudentList"
|
|
|
|
|
|
:key="student.userId"
|
|
|
|
|
|
class="student-item"
|
|
|
|
|
|
@click="goToStudentDetail(student)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="student-avatar">
|
|
|
|
|
|
<text class="avatar-text">{{ (student.nickName || student.userName || 'S').charAt(0) }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="student-info">
|
|
|
|
|
|
<text class="student-name">{{ student.nickName || student.userName }}</text>
|
|
|
|
|
|
<text class="student-id">学号:{{ student.userName }}</text>
|
|
|
|
|
|
<text class="student-class" v-if="student.className">班级:{{ student.className }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="student-actions">
|
|
|
|
|
|
<text class="arrow-icon">›</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import { getAllStudents, getStudentsByClass } from '@/api/study/classUser.js'
|
|
|
|
|
|
import request from '@/utils/request.js'
|
|
|
|
|
|
import CustomNavbar from '@/components/custom-navbar/custom-navbar.vue'
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
components: {
|
|
|
|
|
|
CustomNavbar
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
studentList: [],
|
|
|
|
|
|
filteredStudentList: [],
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
searchKeyword: '',
|
|
|
|
|
|
classList: [],
|
|
|
|
|
|
classOptions: [{ value: null, label: '全部班级' }],
|
|
|
|
|
|
selectedClassId: null,
|
|
|
|
|
|
selectedClassName: null,
|
|
|
|
|
|
showClassPicker: false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
this.loadClassList()
|
|
|
|
|
|
this.loadStudentList()
|
|
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
|
|
|
|
|
this.loadStudentList()
|
|
|
|
|
|
},
|
|
|
|
|
|
onPullDownRefresh() {
|
|
|
|
|
|
this.loadStudentList().finally(() => {
|
|
|
|
|
|
uni.stopPullDownRefresh()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
async loadClassList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await request.get('/study/class/list')
|
|
|
|
|
|
if (response.code === 200) {
|
|
|
|
|
|
this.classList = response.rows || response.data || []
|
|
|
|
|
|
// 构建班级选项
|
|
|
|
|
|
this.classOptions = [
|
|
|
|
|
|
{ value: null, label: '全部班级' },
|
|
|
|
|
|
...this.classList.map(cls => ({
|
|
|
|
|
|
value: cls.id,
|
|
|
|
|
|
label: cls.className
|
|
|
|
|
|
}))
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载班级列表失败', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async loadStudentList() {
|
|
|
|
|
|
this.loading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
let response
|
|
|
|
|
|
if (this.selectedClassId) {
|
|
|
|
|
|
// 按班级获取学生
|
|
|
|
|
|
response = await getStudentsByClass(this.selectedClassId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 获取所有学生
|
|
|
|
|
|
response = await getAllStudents()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (response.code === 200) {
|
|
|
|
|
|
this.studentList = response.data || []
|
|
|
|
|
|
this.applyFilter()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: response.msg || '加载失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载学生列表失败', error)
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: error.message || '加载失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
applyFilter() {
|
|
|
|
|
|
let filtered = [...this.studentList]
|
|
|
|
|
|
|
|
|
|
|
|
// 按关键词搜索
|
|
|
|
|
|
if (this.searchKeyword) {
|
|
|
|
|
|
const keyword = this.searchKeyword.toLowerCase()
|
|
|
|
|
|
filtered = filtered.filter(student => {
|
|
|
|
|
|
const name = (student.nickName || student.userName || '').toLowerCase()
|
|
|
|
|
|
const username = (student.userName || '').toLowerCase()
|
|
|
|
|
|
return name.includes(keyword) || username.includes(keyword)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.filteredStudentList = filtered
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleSearch(value) {
|
|
|
|
|
|
// 如果传入了值,更新搜索关键词
|
|
|
|
|
|
if (value !== undefined) {
|
|
|
|
|
|
this.searchKeyword = value
|
|
|
|
|
|
}
|
|
|
|
|
|
this.applyFilter()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onClassConfirm(e) {
|
|
|
|
|
|
const selected = e.value[0]
|
|
|
|
|
|
this.selectedClassId = selected.value
|
|
|
|
|
|
this.selectedClassName = selected.label
|
|
|
|
|
|
this.showClassPicker = false
|
|
|
|
|
|
this.loadStudentList()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
goToStudentDetail(student) {
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: `/pages/student/detail?id=${student.userId}`
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.student-list-container {
|
|
|
|
|
|
padding: 30rpx;
|
|
|
|
|
|
padding-top: calc(30rpx + 88rpx + env(safe-area-inset-top));
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
padding: 60rpx;
|
|
|
|
|
|
max-width: 1200rpx;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-section {
|
|
|
|
|
|
margin-bottom: 30rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.search-box {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 50rpx;
|
|
|
|
|
|
padding: 20rpx 30rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
2025-12-10 13:15:26 +08:00
|
|
|
|
|
|
|
|
|
|
.search-icon {
|
|
|
|
|
|
font-size: 36rpx;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
2025-12-03 18:58:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-section {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
padding: 30rpx;
|
|
|
|
|
|
margin-bottom: 30rpx;
|
|
|
|
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
|
|
|
|
|
|
.filter-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 20rpx 24rpx;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.filter-label {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-right: 12rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-value {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-icon {
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
margin-left: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.student-list {
|
|
|
|
|
|
.student-item {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
padding: 30rpx;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.student-avatar {
|
|
|
|
|
|
width: 100rpx;
|
|
|
|
|
|
height: 100rpx;
|
|
|
|
|
|
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: 24rpx;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-text {
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.student-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.student-name {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.student-id {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.student-class {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: rgb(55 140 224);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.student-actions {
|
|
|
|
|
|
.arrow-icon {
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
color: #d9d9d9;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|