feat: 添加班级管理搜索,调整学员库数据,批量删除,分页器显示,修复侧边栏高度问题

This commit is contained in:
QDKF 2025-09-16 19:28:31 +08:00
parent 68c64a96c1
commit 7e540664e2
4 changed files with 748 additions and 282 deletions

View File

@ -42,10 +42,12 @@
<n-divider v-if="props.type === 'course'" />
<div class="toolbar">
<div class="student-count" v-if="props.type === 'course'">
全部学员{{ totalStudents }}
<span v-if="!searchKeyword">全部学员{{ totalStudents }}</span>
<span v-else>搜索结果{{ filteredData.length }}{{ totalStudents }}</span>
</div>
<div class="student-title" v-if="props.type === 'student'">
全部学员
<span v-if="!searchKeyword">全部学员</span>
<span v-else>搜索结果{{ filteredData.length }}</span>
</div>
<NSpace>
<n-dropdown trigger="hover" :options="addStudentOptions" @select="handleAddStudentSelect"
@ -57,8 +59,7 @@
<n-button type="primary" @click="openAddModal" v-else-if="props.type === 'student'">
添加学员
</n-button>
<n-button type="primary" ghost v-if="props.type === 'student'" @click="handleStatisticsAnalysis">
统计分析
<n-button type="primary" ghost v-if="props.type === 'student'" @click="handleStatisticsAnalysis"> 统计分析
</n-button>
<n-button type="primary" ghost @click="showImportModal = true">
导入
@ -66,15 +67,19 @@
<n-button type="primary" ghost>
导出
</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px" />
<n-button type="primary">
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px"
@input="handleSearch" />
<n-button type="primary" @click="handleSearch">
搜索
</n-button>
<n-button v-if="searchKeyword" @click="clearSearch" type="default">
清空
</n-button>
</NSpace>
</div>
<n-divider v-if="props.type === 'student'" />
<n-data-table :columns="columns" :data="data" :pagination="pagination" :loading="loading"
<n-data-table :columns="columns" :data="paginatedData" :pagination="pagination" :loading="loading"
:row-key="(row: StudentItem) => row.id" v-model:checked-row-keys="selectedRowKeys" striped bordered
size="small" />
@ -335,8 +340,8 @@ interface FormData {
className: string[] //
}
const totalStudents = ref(1333)
const inviteCode = ref('56685222')
const totalStudents = ref(0)
const inviteCode = ref('')
const currentInviteClassId = ref<string | null>(null) // ID
const message = useMessage()
@ -439,6 +444,29 @@ const selectedStudents = computed(() => {
return data.value.filter(student => selectedRowKeys.value.includes(student.id))
})
//
const filteredData = computed(() => {
if (!searchKeyword.value.trim()) {
return data.value
}
const keyword = searchKeyword.value.trim().toLowerCase()
return data.value.filter(student =>
student.studentName.toLowerCase().includes(keyword) ||
student.accountNumber.toLowerCase().includes(keyword) ||
student.loginName.toLowerCase().includes(keyword) ||
student.college.toLowerCase().includes(keyword) ||
student.className.toLowerCase().includes(keyword)
)
})
//
const paginatedData = computed(() => {
const start = (pagination.value.page - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
return filteredData.value.slice(start, end)
})
//
//
@ -618,14 +646,15 @@ const pagination = ref({
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
itemCount: computed(() => filteredData.value.length), // 使
onChange: (page: number) => {
pagination.value.page = page
loadData(props.classId)
//
},
onUpdatePageSize: (pageSize: number) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
loadData(props.classId)
//
}
})
@ -1190,6 +1219,24 @@ const handleTemplateDownload = (type?: string) => {
// TODO:
}
//
const handleSearch = () => {
// filteredData
//
pagination.value.page = 1
}
//
watch(searchKeyword, () => {
pagination.value.page = 1
})
//
const clearSearch = () => {
searchKeyword.value = ''
pagination.value.page = 1
}
// ID
watch(
() => props.classId,
@ -1197,6 +1244,9 @@ watch(
if (newClassId !== oldClassId) {
// watch
selectedDepartment.value = newClassId ? String(newClassId) : ''
//
searchKeyword.value = ''
pagination.value.page = 1
loadData(newClassId)
}
},
@ -1212,6 +1262,9 @@ watch(
// props.classIdprops
const currentPropsClassId = props.classId ? String(props.classId) : ''
if (newDepartmentId !== oldDepartmentId && newDepartmentId !== currentPropsClassId) {
//
searchKeyword.value = ''
pagination.value.page = 1
const targetClassId = newDepartmentId || null
loadData(targetClassId)
}
@ -1482,57 +1535,6 @@ defineExpose({
gap: 8px;
}
.batch-delete-content {
text-align: center;
}
.batch-delete-content p {
margin: 8px 0;
font-size: 14px;
}
.selected-students {
margin: 16px 0;
max-height: 200px;
overflow-y: auto;
}
.student-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.student-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background-color: white;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.student-name {
font-weight: 500;
color: #333;
}
.student-account {
color: #666;
font-size: 12px;
}
.batch-warning {
color: #ff4d4f;
font-weight: 500;
margin-top: 16px;
}
.batch-delete-content {
text-align: center;
padding: 20px 0;

View File

@ -133,7 +133,7 @@
<script setup lang="ts">
// @ts-nocheck
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, computed, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ChevronDownOutline } from '@vicons/ionicons5'
import { useUserStore } from '@/stores/user'
@ -230,6 +230,11 @@ const handleBreadcrumbClick = (path: string) => {
const handleClose = () => {
console.log('关闭按钮被点击');
showTopImage.value = false; //
// CSS
nextTick(() => {
document.documentElement.style.setProperty('--top-height', '0px');
});
}
//
@ -642,6 +647,13 @@ const breadcrumbPathItems = computed(() => {
onMounted(() => {
//
updateActiveNavItem();
// CSS
if (showTopImage.value) {
document.documentElement.style.setProperty('--top-height', '130px');
} else {
document.documentElement.style.setProperty('--top-height', '0px');
}
});
// 使watch
@ -823,7 +835,6 @@ const updateActiveNavItem = () => {
width: 240px;
height: calc(100vh - var(--top-height, 130px));
background: #FFFFFF;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
@media screen and (max-width: 768px) {

View File

@ -2,7 +2,7 @@
<div class="message-center">
<!-- 顶部Tab导航 -->
<div class="tab-container">
<n-tabs v-model:value="activeTab" type="line" size="large" class="message-tabs">
<n-tabs v-model:value="activeTab" type="line" size="large" class="message-tabs" @update:value="handleTabChange">
<n-tab-pane name="notification" tab="即时消息">
<template #tab>
<div class="tab-item">
@ -64,7 +64,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { NBadge, NTabs, NTabPane, useMessage } from 'naive-ui'
import { ChatApi } from '@/api'
@ -87,24 +87,44 @@ const systemCount = ref(0) // 系统消息数量
//
const loading = ref(false)
//
const cacheTimestamp = ref(0)
const CACHE_DURATION = 30000 // 30
let refreshTimer: NodeJS.Timeout | null = null
//
onMounted(() => {
//
loadMessageCounts()
})
onUnmounted(() => {
//
if (refreshTimer) {
clearTimeout(refreshTimer)
refreshTimer = null
}
})
//
const loadMessageCounts = async () => {
const loadMessageCounts = async (forceRefresh = false) => {
//
const now = Date.now()
if (!forceRefresh && now - cacheTimestamp.value < CACHE_DURATION) {
return
}
loading.value = true
try {
//
await loadNotificationCount()
// TODO:
// await loadCommentCount()
// await loadFavoriteCount()
// await loadSystemCount()
//
await Promise.allSettled([
loadNotificationCount(),
loadCommentCount(),
loadFavoriteCount(),
loadSystemCount()
])
cacheTimestamp.value = now
} catch (error) {
console.error('加载消息数量失败:', error)
message.error('加载消息数量失败')
@ -113,29 +133,80 @@ const loadMessageCounts = async () => {
}
}
//
const debouncedRefresh = () => {
if (refreshTimer) {
clearTimeout(refreshTimer)
}
refreshTimer = setTimeout(() => {
loadMessageCounts(true)
}, 1000)
}
//
const loadNotificationCount = async () => {
try {
const response = await ChatApi.getUnreadCount()
if (response.data) {
notificationCount.value = response.data.total || 0
return
}
} catch (error) {
console.error('获取未读消息数量失败:', error)
// API
console.warn('获取未读消息数量失败,尝试备用方案:', error)
}
//
try {
const chatsResponse = await ChatApi.getMyChats()
if (chatsResponse.data && chatsResponse.data.success) {
if (chatsResponse.data?.success) {
notificationCount.value = chatsResponse.data.result.reduce((total: number, chat: any) => {
return total + (chat.unreadCount || 0)
}, 0)
}
} catch (chatError) {
console.error('获取会话列表失败:', chatError)
// 0
notificationCount.value = 0
}
}
// @
const loadCommentCount = async () => {
try {
// TODO: @API
commentCount.value = 0
} catch (error) {
console.error('获取评论数量失败:', error)
commentCount.value = 0
}
}
//
const loadFavoriteCount = async () => {
try {
// TODO: API
favoriteCount.value = 0
} catch (error) {
console.error('获取收藏数量失败:', error)
favoriteCount.value = 0
}
}
//
const loadSystemCount = async () => {
try {
// TODO: API
systemCount.value = 0
} catch (error) {
console.error('获取系统消息数量失败:', error)
systemCount.value = 0
}
}
// Tab
const handleTabChange = (tabName: string) => {
activeTab.value = tabName
// Tab
debouncedRefresh()
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="student-library" v-if="false">
<div class="student-library">
<!-- 页面标题 -->
<div class="header-section" :bordered="false">
<h1 class="page-title">全部学员</h1>
@ -8,8 +8,10 @@
<n-button type="primary" ghost @click="handleStats">统计分析</n-button>
<n-button type="primary" ghost @click="handleExport">导入</n-button>
<n-button type="primary" ghost @click="handleImport">导出</n-button>
<n-button type="error" ghost @click="handleBatchDelete">删除</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入人员姓名学号" style="width: 200px;"
<n-button type="error" ghost @click="handleBatchDelete" :disabled="checkedRowKeys.length === 0">
删除({{ checkedRowKeys.length }})
</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px;"
@keyup.enter="handleSearch">
<template #suffix>
<n-button text @click="handleSearch">
@ -27,23 +29,104 @@
<!-- 数据表格 -->
<div class="table-card" :bordered="false">
<n-data-table :columns="columns" :data="filteredStudentList" :loading="loading"
:pagination="paginationReactive" :row-key="rowKey" :checked-row-keys="checkedRowKeys"
@update:checked-row-keys="handleCheck" striped size="medium" />
<n-data-table :columns="columns" :data="studentList" :loading="loading" :row-key="rowKey"
:checked-row-keys="checkedRowKeys" @update:checked-row-keys="handleCheck" striped size="medium" />
</div>
<!-- 独立的分页器 -->
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
<n-pagination v-model:page="pagination.page" v-model:page-size="pagination.pageSize"
:item-count="pagination.total" :page-sizes="[10, 20, 50, 100]" show-size-picker show-quick-jumper
:prefix="(info: any) => `共 ${info.itemCount} 条`" @update:page="handlePageChange"
@update:page-size="handlePageSizeChange" />
</div>
<div class="student-library" v-else>
<!-- TODO: 暂时传id为1来显示模拟数据对接接口需要去掉 -->
<ClassManagement type="student" :class-id="1"></ClassManagement>
<!-- 添加学员弹窗 -->
<n-modal v-model:show="showAddModal" :title="isEditMode ? '编辑学员' : '添加学员'">
<n-card style="width: 600px" :title="isEditMode ? '编辑学员' : '添加学员'" :bordered="false" size="huge"
role="dialog" aria-modal="true">
<n-form ref="formRef" :model="formData" :rules="rules" label-placement="left" label-width="80px"
require-mark-placement="right-hanging">
<n-form-item label="姓名" path="studentName">
<n-input v-model:value="formData.studentName" placeholder="请输入学员姓名" clearable />
</n-form-item>
<n-form-item label="学号" path="studentId">
<n-input v-model:value="formData.studentId" placeholder="请输入学员学号" clearable />
</n-form-item>
<n-form-item label="登录密码" path="loginPassword">
<n-input v-model:value="formData.loginPassword" type="password"
:placeholder="isEditMode ? '不填写则不修改密码' : '请输入登录密码'" show-password-on="click" clearable />
</n-form-item>
<n-form-item label="所在学校" path="college">
<n-select v-model:value="formData.college" :options="collegeOptions" placeholder="请选择学校"
clearable />
</n-form-item>
<n-form-item label="所在班级" path="className">
<n-select v-model:value="formData.className" :options="classSelectOptions" placeholder="请选择班级"
multiple clearable />
</n-form-item>
</n-form>
<template #footer>
<div class="modal-footer">
<n-button @click="closeAddModal">取消</n-button>
<n-button type="primary" @click="handleSubmit">
{{ isEditMode ? '保存' : '添加' }}
</n-button>
</div>
</template>
</n-card>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h, computed } from 'vue'
import { NButton, useMessage, useDialog } from 'naive-ui'
import { ref, reactive, onMounted, h, computed, watch, nextTick } from 'vue'
import {
NButton,
useMessage,
useDialog,
NSpace,
NModal,
NCard,
NForm,
NFormItem,
NInput,
NSelect,
NPagination,
type FormInst,
type FormRules
} from 'naive-ui'
import { SearchOutline } from '@vicons/ionicons5'
import ClassManagement from '@/components/teacher/ClassManagement.vue'
import { ClassApi, TeachCourseApi } from '@/api/modules/teachCourse'
//
interface StudentItem {
id: string
studentName: string
accountNumber: string
className: string
college: string
loginName: string
joinTime: string
}
//
interface ClassItem {
id: string
className: string
studentCount: number
creator: string
createTime: string
}
//
interface FormData {
studentName: string
studentId: string
loginPassword: string
college: string
className: string[] //
}
const message = useMessage()
const dialog = useDialog()
@ -55,153 +138,152 @@ const searchKeyword = ref('')
const loading = ref(false)
const checkedRowKeys = ref<Array<string | number>>([])
//
const studentList = ref([
{
id: 1,
sequence: 1,
name: '王琪琨',
studentId: '18653354882',
gender: '男',
school: '北京大学',
class: '北京清华大学-班级—/北京清华大学-班级—/北京清华大学-班级—',
joinTime: '2025.07.25 09:20',
status: 'active'
},
{
id: 2,
sequence: 2,
name: '李明',
studentId: '18653354883',
gender: '女',
school: '清华大学',
class: '清华大学-软件工程-1班/清华大学-软件工程-2班',
joinTime: '2025.07.26 10:15',
status: 'active'
},
{
id: 3,
sequence: 3,
name: '张伟',
studentId: '18653354884',
gender: '男',
school: '复旦大学',
class: '复旦大学-计算机科学-A班',
joinTime: '2025.07.27 14:30',
status: 'active'
},
{
id: 4,
sequence: 4,
name: '刘红',
studentId: '18653354885',
gender: '女',
school: '上海交通大学',
class: '上海交通大学-信息工程-1班/上海交通大学-信息工程-2班',
joinTime: '2025.07.28 09:45',
status: 'active'
},
{
id: 5,
sequence: 5,
name: '陈小明',
studentId: '18653354886',
gender: '男',
school: '浙江大学',
class: '浙江大学-电子信息-1班',
joinTime: '2025.07.29 11:20',
status: 'active'
},
{
id: 6,
sequence: 6,
name: '王小丽',
studentId: '18653354887',
gender: '女',
school: '中山大学',
class: '中山大学-软件工程-A班/中山大学-软件工程-B班',
joinTime: '2025.07.30 16:10',
status: 'active'
}
])
//
const studentList = ref<StudentItem[]>([])
const classList = ref<ClassItem[]>([])
//
const totalStudents = ref(0)
//
const paginationReactive = reactive({
const pagination = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: (page: number) => {
paginationReactive.page = page
},
onUpdatePageSize: (pageSize: number) => {
paginationReactive.pageSize = pageSize
paginationReactive.page = 1
}
total: 0
})
//
const filteredStudentList = computed(() => {
if (!searchKeyword.value) {
return studentList.value
}
return studentList.value.filter(student =>
student.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
student.studentId.includes(searchKeyword.value)
)
// key
const paginationKey = ref(0)
// pagination.total
watch(() => pagination.total, () => {
nextTick(() => {
paginationKey.value++
})
})
//
const showAddModal = ref(false)
const isEditMode = ref(false)
const currentEditId = ref('')
const formRef = ref<FormInst | null>(null)
//
const formData = ref<FormData>({
studentName: '',
studentId: '',
loginPassword: '',
college: '',
className: [] //
})
//
const collegeOptions = ref([])
//
const classSelectOptions = computed(() =>
classList.value.map((item: ClassItem) => ({
label: item.className,
value: item.id
}))
)
//
const rules: FormRules = {
studentName: [
{ required: true, message: '请输入学员姓名', trigger: 'blur' }
],
studentId: [
{ required: true, message: '请输入学员学号', trigger: 'blur' }
],
loginPassword: [
// { required: true, message: '', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
college: [
{ required: true, message: '请选择所在学院', trigger: 'blur' }
],
className: [
{
required: true,
type: 'array',
min: 1,
message: '请选择至少一个班级',
trigger: 'blur'
}
]
}
// loadAllStudents
// key
const rowKey = (row: any) => row.id
// ID
const getClassNameById = (classId: string): string => {
const classItem = classList.value.find((item: ClassItem) => item.id === classId)
return classItem ? classItem.className : classId
}
//
const formatClassNames = (classInfo: string): string[] => {
if (!classInfo) return ['未分配班级']
if (classInfo.includes(',')) {
//
return classInfo.split(',').map(id => id.trim()).map(getClassNameById)
} else {
//
return [getClassNameById(classInfo)]
}
}
//
const columns = [
{
type: 'selection'
},
{
title: '序号',
key: 'sequence',
width: 80,
align: 'center'
},
{
title: '姓名',
key: 'name',
width: 100,
align: 'center'
},
{
title: '学号',
key: 'studentId',
width: 140,
align: 'center'
},
{
title: '性别',
key: 'gender',
width: 80,
align: 'center'
},
{
title: '所属学校',
key: 'school',
key: 'studentName',
width: 120,
align: 'center'
},
{
title: '所属班级',
key: 'class',
width: 300,
title: '账号',
key: 'accountNumber',
width: 140,
align: 'center'
},
{
title: '班级',
key: 'className',
width: 150,
align: 'center',
ellipsis: {
tooltip: true
render: (row: StudentItem) => {
// 使
const classNames = formatClassNames(row.className || '')
//
return h('div', {
class: 'class-cell'
}, classNames.map((name, index) =>
h('div', {
key: index,
class: 'class-cell-item'
}, name)
))
}
},
{
title: '所在学院',
key: 'college',
width: 200,
align: 'center'
},
{
title: '加入时间',
key: 'joinTime',
width: 160,
width: 140,
align: 'center'
},
{
@ -209,8 +291,12 @@ const columns = [
key: 'actions',
width: 200,
align: 'center',
render(row: any) {
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, [
render: (row: StudentItem) => {
return h(
NSpace,
{ size: 'small', justify: 'center' },
{
default: () => [
h(
NButton,
{
@ -241,19 +327,236 @@ const columns = [
},
{ default: () => '删除' }
)
])
]
}
)
}
}
]
//
const handleSearch = () => {
// computed
message.success('搜索完成')
//
const loadClassList = async () => {
try {
console.log('🚀 开始加载班级列表数据...')
const response = await ClassApi.queryClassList({ course_id: null })
// API
const classListData = response.data.result || []
const transformedClassData: ClassItem[] = classListData.map((classItem: any) => ({
id: classItem.id?.toString() || '',
className: classItem.name || '未知班级',
studentCount: classItem.studentCount || 0,
creator: classItem.createBy || '未知创建者',
createTime: classItem.createTime ? new Date(classItem.createTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '') : new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '')
}))
classList.value = transformedClassData
console.log(`✅ 成功加载班级列表,共 ${transformedClassData.length} 个班级`)
} catch (error) {
console.error('❌ 加载班级列表失败:', error)
message.error('加载班级列表失败,请重试')
classList.value = []
}
}
//
const loadSchoolList = () => {
TeachCourseApi.getSchoolList().then(res => {
collegeOptions.value = res.data.result.map((school: any) => ({
label: school,
value: school
}))
}).catch(err => {
console.error('获取学校列表失败:', err)
})
}
//
const loadAllStudents = async () => {
loading.value = true
try {
console.log('🚀 开始加载所有班级的学员数据...')
//
await loadClassList()
if (classList.value.length === 0) {
studentList.value = []
totalStudents.value = 0
return
}
//
const allStudents: StudentItem[] = []
for (const classItem of classList.value) {
try {
const response = await ClassApi.getClassStudents(classItem.id)
const studentsData = response.data.result || []
const transformedStudents: StudentItem[] = studentsData.map((student: any) => ({
id: student.id || '',
studentName: student.realname || student.username || '未知姓名',
accountNumber: student.studentId || student.username || '',
className: classItem.id, // 使ID
college: student.college || student.department || '未分配学院',
loginName: student.username || '',
joinTime: student.createTime ? new Date(student.createTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '') : '未知时间'
}))
allStudents.push(...transformedStudents)
} catch (error) {
console.error(`❌ 加载班级 ${classItem.className} 的学员数据失败:`, error)
}
}
//
let filteredData = allStudents
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.trim().toLowerCase()
filteredData = allStudents.filter((student: StudentItem) =>
student.studentName.toLowerCase().includes(keyword) ||
student.accountNumber.toLowerCase().includes(keyword) ||
student.loginName.toLowerCase().includes(keyword) ||
student.college.toLowerCase().includes(keyword) ||
student.className.toLowerCase().includes(keyword)
)
}
//
pagination.total = filteredData.length
const start = (pagination.page - 1) * pagination.pageSize
const end = start + pagination.pageSize
studentList.value = filteredData.slice(start, end)
totalStudents.value = allStudents.length
console.log(`✅ 成功加载所有学员数据,共 ${allStudents.length} 名学员`)
} catch (error) {
console.error('❌ 加载学员数据失败:', error)
message.error('加载学员数据失败,请重试')
studentList.value = []
totalStudents.value = 0
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
//
pagination.page = 1
loadAllStudents()
}
//
const handlePageChange = (page: number) => {
pagination.page = page
loadAllStudents()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.page = 1
loadAllStudents()
}
//
const handleAddStudent = () => {
message.info('添加学员功能开发中...')
resetForm()
showAddModal.value = true
}
//
const resetForm = () => {
formData.value = {
studentName: '',
studentId: '',
loginPassword: '',
college: '',
className: [] //
}
isEditMode.value = false
currentEditId.value = ''
}
//
const closeAddModal = () => {
showAddModal.value = false
resetForm()
}
// /
const handleSubmit = async () => {
try {
await formRef.value?.validate()
if (isEditMode.value) {
//
message.info('编辑功能暂未实现,敬请期待')
return
} else {
//
console.log('🚀 开始新增学员...')
console.log('表单数据:', formData.value)
//
if (!formData.value.className || formData.value.className.length === 0) {
message.error('请选择班级')
return
}
// API
const payload = {
realName: formData.value.studentName,
studentNumber: formData.value.studentId,
password: formData.value.loginPassword,
school: formData.value.college,
classId: formData.value.className.join(',')
}
console.log('📝 API请求参数:', payload)
// API
const response = await ClassApi.createdStudents(payload)
console.log('✅ 创建学员响应:', response)
if (response.data && (response.data.success || response.data.code === 200)) {
message.success(`已成功添加学员 ${formData.value.studentName}`)
} else {
message.error(response.data?.message || '添加学员失败')
return
}
}
//
showAddModal.value = false
resetForm()
//
loadAllStudents()
} catch (error: any) {
console.error('❌ 添加学员失败:', error)
message.error(error.message || '添加学员失败,请重试')
}
}
const handleStats = () => {
@ -279,13 +582,33 @@ const handleBatchDelete = () => {
content: `确定要删除选中的 ${checkedRowKeys.value.length} 名学员吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
//
studentList.value = studentList.value.filter(student =>
!checkedRowKeys.value.includes(student.id)
onPositiveClick: async () => {
try {
//
const selectedStudents = studentList.value.filter((student: StudentItem) =>
checkedRowKeys.value.includes(student.id)
)
//
for (const student of selectedStudents) {
// ID
const classId = student.className
if (classId) {
await ClassApi.removeStudent(classId, student.accountNumber)
}
}
message.success(`成功删除 ${selectedStudents.length} 名学员`)
//
checkedRowKeys.value = []
message.success('删除成功')
//
loadAllStudents()
} catch (error) {
console.error('批量删除失败:', error)
message.error('批量删除失败,请重试')
}
}
})
}
@ -294,33 +617,59 @@ const handleCheck = (keys: Array<string | number>) => {
checkedRowKeys.value = keys
}
const handleViewProgress = (row: any) => {
message.info(`查看 ${row.name} 的学习进度`)
const handleViewProgress = (row: StudentItem) => {
message.info(`查看 ${row.studentName} 的学习进度`)
}
const handleEditStudent = (row: any) => {
message.info(`编辑学员 ${row.name}`)
const handleEditStudent = (row: StudentItem) => {
isEditMode.value = true
currentEditId.value = row.id
//
formData.value = {
studentName: row.studentName,
studentId: row.accountNumber,
loginPassword: '', //
college: row.college,
className: [row.className] //
}
showAddModal.value = true
}
const handleDeleteStudent = (row: any) => {
const handleDeleteStudent = (row: StudentItem) => {
dialog.warning({
title: '确认删除',
content: `确定要删除学员 ${row.name} 吗?`,
content: `确定要删除学员 ${row.studentName} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
const index = studentList.value.findIndex(student => student.id === row.id)
if (index > -1) {
studentList.value.splice(index, 1)
message.success('删除成功')
onPositiveClick: async () => {
try {
// ID
const classId = row.className
if (!classId) {
message.error('无法确定学员所在班级,删除失败')
return
}
//
await ClassApi.removeStudent(classId, row.accountNumber)
message.success(`已删除学员:${row.studentName}`)
//
loadAllStudents()
} catch (error) {
console.error('删除学员失败:', error)
message.error('删除失败,请重试')
}
}
})
}
onMounted(() => {
//
loading.value = false
//
loadSchoolList()
//
loadAllStudents()
})
</script>
@ -360,4 +709,37 @@ onMounted(() => {
align-items: center;
gap: 12px;
}
/* 班级列样式 */
.class-cell {
line-height: 1.4;
word-break: break-all;
white-space: normal;
max-width: 150px;
min-height: 20px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 4px 8px;
}
.class-cell-item {
padding: 1px 0;
font-size: 13px;
color: #333;
border-radius: 2px;
margin: 1px 0;
}
.class-cell-item:not(:last-child) {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 2px;
margin-bottom: 2px;
}
.modal-footer {
display: flex;
justify-content: end;
gap: 12px;
}
</style>