feat: 添加班级管理搜索,调整学员库数据,批量删除,分页器显示,修复侧边栏高度问题
This commit is contained in:
parent
68c64a96c1
commit
7e540664e2
@ -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.classId存在且与选择器值一致,说明是props驱动的变化,不需要重复加载
|
||||
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;
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user