feat: 添加班级管理搜索,调整学员库数据,批量删除,分页器显示,修复侧边栏高度问题
This commit is contained in:
parent
68c64a96c1
commit
7e540664e2
@ -42,10 +42,12 @@
|
|||||||
<n-divider v-if="props.type === 'course'" />
|
<n-divider v-if="props.type === 'course'" />
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="student-count" v-if="props.type === 'course'">
|
<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>
|
||||||
<div class="student-title" v-if="props.type === 'student'">
|
<div class="student-title" v-if="props.type === 'student'">
|
||||||
全部学员
|
<span v-if="!searchKeyword">全部学员</span>
|
||||||
|
<span v-else>搜索结果{{ filteredData.length }}人</span>
|
||||||
</div>
|
</div>
|
||||||
<NSpace>
|
<NSpace>
|
||||||
<n-dropdown trigger="hover" :options="addStudentOptions" @select="handleAddStudentSelect"
|
<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 type="primary" @click="openAddModal" v-else-if="props.type === 'student'">
|
||||||
添加学员
|
添加学员
|
||||||
</n-button>
|
</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>
|
||||||
<n-button type="primary" ghost @click="showImportModal = true">
|
<n-button type="primary" ghost @click="showImportModal = true">
|
||||||
导入
|
导入
|
||||||
@ -66,15 +67,19 @@
|
|||||||
<n-button type="primary" ghost>
|
<n-button type="primary" ghost>
|
||||||
导出
|
导出
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px" />
|
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px"
|
||||||
<n-button type="primary">
|
@input="handleSearch" />
|
||||||
|
<n-button type="primary" @click="handleSearch">
|
||||||
搜索
|
搜索
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button v-if="searchKeyword" @click="clearSearch" type="default">
|
||||||
|
清空
|
||||||
|
</n-button>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</div>
|
</div>
|
||||||
<n-divider v-if="props.type === 'student'" />
|
<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
|
:row-key="(row: StudentItem) => row.id" v-model:checked-row-keys="selectedRowKeys" striped bordered
|
||||||
size="small" />
|
size="small" />
|
||||||
|
|
||||||
@ -335,8 +340,8 @@ interface FormData {
|
|||||||
className: string[] // 修改为数组类型支持多选
|
className: string[] // 修改为数组类型支持多选
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalStudents = ref(1333)
|
const totalStudents = ref(0)
|
||||||
const inviteCode = ref('56685222')
|
const inviteCode = ref('')
|
||||||
const currentInviteClassId = ref<string | null>(null) // 当前邀请码对应的班级ID
|
const currentInviteClassId = ref<string | null>(null) // 当前邀请码对应的班级ID
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@ -439,6 +444,29 @@ const selectedStudents = computed(() => {
|
|||||||
return data.value.filter(student => selectedRowKeys.value.includes(student.id))
|
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,
|
pageSize: 10,
|
||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
pageSizes: [10, 20, 50],
|
pageSizes: [10, 20, 50],
|
||||||
|
itemCount: computed(() => filteredData.value.length), // 使用过滤后的数据长度
|
||||||
onChange: (page: number) => {
|
onChange: (page: number) => {
|
||||||
pagination.value.page = page
|
pagination.value.page = page
|
||||||
loadData(props.classId)
|
// 前端分页不需要重新加载数据
|
||||||
},
|
},
|
||||||
onUpdatePageSize: (pageSize: number) => {
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
pagination.value.pageSize = pageSize
|
pagination.value.pageSize = pageSize
|
||||||
pagination.value.page = 1
|
pagination.value.page = 1
|
||||||
loadData(props.classId)
|
// 前端分页不需要重新加载数据
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1190,6 +1219,24 @@ const handleTemplateDownload = (type?: string) => {
|
|||||||
// TODO: 实现模板下载逻辑
|
// TODO: 实现模板下载逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
// 搜索是实时的,通过计算属性filteredData自动过滤
|
||||||
|
// 重置到第一页
|
||||||
|
pagination.value.page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听搜索关键词变化,重置分页
|
||||||
|
watch(searchKeyword, () => {
|
||||||
|
pagination.value.page = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空搜索
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchKeyword.value = ''
|
||||||
|
pagination.value.page = 1
|
||||||
|
}
|
||||||
|
|
||||||
// 监听班级ID变化,重新加载数据
|
// 监听班级ID变化,重新加载数据
|
||||||
watch(
|
watch(
|
||||||
() => props.classId,
|
() => props.classId,
|
||||||
@ -1197,6 +1244,9 @@ watch(
|
|||||||
if (newClassId !== oldClassId) {
|
if (newClassId !== oldClassId) {
|
||||||
// 同步更新选择器的状态(不触发选择器的watch)
|
// 同步更新选择器的状态(不触发选择器的watch)
|
||||||
selectedDepartment.value = newClassId ? String(newClassId) : ''
|
selectedDepartment.value = newClassId ? String(newClassId) : ''
|
||||||
|
// 切换班级时清空搜索
|
||||||
|
searchKeyword.value = ''
|
||||||
|
pagination.value.page = 1
|
||||||
loadData(newClassId)
|
loadData(newClassId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1212,6 +1262,9 @@ watch(
|
|||||||
// 如果当前props.classId存在且与选择器值一致,说明是props驱动的变化,不需要重复加载
|
// 如果当前props.classId存在且与选择器值一致,说明是props驱动的变化,不需要重复加载
|
||||||
const currentPropsClassId = props.classId ? String(props.classId) : ''
|
const currentPropsClassId = props.classId ? String(props.classId) : ''
|
||||||
if (newDepartmentId !== oldDepartmentId && newDepartmentId !== currentPropsClassId) {
|
if (newDepartmentId !== oldDepartmentId && newDepartmentId !== currentPropsClassId) {
|
||||||
|
// 切换班级时清空搜索
|
||||||
|
searchKeyword.value = ''
|
||||||
|
pagination.value.page = 1
|
||||||
const targetClassId = newDepartmentId || null
|
const targetClassId = newDepartmentId || null
|
||||||
loadData(targetClassId)
|
loadData(targetClassId)
|
||||||
}
|
}
|
||||||
@ -1482,57 +1535,6 @@ defineExpose({
|
|||||||
gap: 8px;
|
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 {
|
.batch-delete-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ChevronDownOutline } from '@vicons/ionicons5'
|
import { ChevronDownOutline } from '@vicons/ionicons5'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
@ -230,6 +230,11 @@ const handleBreadcrumbClick = (path: string) => {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
console.log('关闭按钮被点击');
|
console.log('关闭按钮被点击');
|
||||||
showTopImage.value = false; // 隐藏顶部图片容器
|
showTopImage.value = false; // 隐藏顶部图片容器
|
||||||
|
|
||||||
|
// 动态更新CSS变量,让侧边栏占满全高
|
||||||
|
nextTick(() => {
|
||||||
|
document.documentElement.style.setProperty('--top-height', '0px');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否隐藏左侧侧边栏
|
// 判断是否隐藏左侧侧边栏
|
||||||
@ -642,6 +647,13 @@ const breadcrumbPathItems = computed(() => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始设置
|
// 初始设置
|
||||||
updateActiveNavItem();
|
updateActiveNavItem();
|
||||||
|
|
||||||
|
// 初始化CSS变量
|
||||||
|
if (showTopImage.value) {
|
||||||
|
document.documentElement.style.setProperty('--top-height', '130px');
|
||||||
|
} else {
|
||||||
|
document.documentElement.style.setProperty('--top-height', '0px');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用watch监听路由变化
|
// 使用watch监听路由变化
|
||||||
@ -823,7 +835,6 @@ const updateActiveNavItem = () => {
|
|||||||
width: 240px;
|
width: 240px;
|
||||||
height: calc(100vh - var(--top-height, 130px));
|
height: calc(100vh - var(--top-height, 130px));
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="message-center">
|
<div class="message-center">
|
||||||
<!-- 顶部Tab导航 -->
|
<!-- 顶部Tab导航 -->
|
||||||
<div class="tab-container">
|
<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="即时消息">
|
<n-tab-pane name="notification" tab="即时消息">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<div class="tab-item">
|
<div class="tab-item">
|
||||||
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<n-tab-pane name="comment" tab="评论和@">
|
<n-tab-pane name="comment" tab="评论和@">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<div class="tab-item">
|
<div class="tab-item">
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<n-tab-pane name="favorite" tab="赞和收藏">
|
<n-tab-pane name="favorite" tab="赞和收藏">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<div class="tab-item">
|
<div class="tab-item">
|
||||||
@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<n-tab-pane name="system" tab="系统消息">
|
<n-tab-pane name="system" tab="系统消息">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<div class="tab-item">
|
<div class="tab-item">
|
||||||
@ -44,7 +44,7 @@
|
|||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab内容区域 -->
|
<!-- Tab内容区域 -->
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div v-show="activeTab === 'notification'">
|
<div v-show="activeTab === 'notification'">
|
||||||
@ -64,7 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { NBadge, NTabs, NTabPane, useMessage } from 'naive-ui'
|
import { NBadge, NTabs, NTabPane, useMessage } from 'naive-ui'
|
||||||
import { ChatApi } from '@/api'
|
import { ChatApi } from '@/api'
|
||||||
|
|
||||||
@ -87,24 +87,44 @@ const systemCount = ref(0) // 系统消息数量
|
|||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 缓存和防抖
|
||||||
|
const cacheTimestamp = ref(0)
|
||||||
|
const CACHE_DURATION = 30000 // 30秒缓存
|
||||||
|
let refreshTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化逻辑
|
// 初始化逻辑
|
||||||
loadMessageCounts()
|
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
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 加载即时消息数量
|
// 并行加载各类消息数量
|
||||||
await loadNotificationCount()
|
await Promise.allSettled([
|
||||||
|
loadNotificationCount(),
|
||||||
// TODO: 后续可以添加其他类型的消息数量加载
|
loadCommentCount(),
|
||||||
// await loadCommentCount()
|
loadFavoriteCount(),
|
||||||
// await loadFavoriteCount()
|
loadSystemCount()
|
||||||
// await loadSystemCount()
|
])
|
||||||
|
|
||||||
|
cacheTimestamp.value = now
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载消息数量失败:', error)
|
console.error('加载消息数量失败:', error)
|
||||||
message.error('加载消息数量失败')
|
message.error('加载消息数量失败')
|
||||||
@ -113,29 +133,80 @@ const loadMessageCounts = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 防抖刷新
|
||||||
|
const debouncedRefresh = () => {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearTimeout(refreshTimer)
|
||||||
|
}
|
||||||
|
refreshTimer = setTimeout(() => {
|
||||||
|
loadMessageCounts(true)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载即时消息数量
|
// 加载即时消息数量
|
||||||
const loadNotificationCount = async () => {
|
const loadNotificationCount = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await ChatApi.getUnreadCount()
|
const response = await ChatApi.getUnreadCount()
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
notificationCount.value = response.data.total || 0
|
notificationCount.value = response.data.total || 0
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取未读消息数量失败:', error)
|
console.warn('获取未读消息数量失败,尝试备用方案:', error)
|
||||||
// 如果API调用失败,尝试获取会话列表计算未读数量
|
|
||||||
try {
|
|
||||||
const chatsResponse = await ChatApi.getMyChats()
|
|
||||||
if (chatsResponse.data && 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 备用方案:通过会话列表计算未读数量
|
||||||
|
try {
|
||||||
|
const chatsResponse = await ChatApi.getMyChats()
|
||||||
|
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)
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="student-library" v-if="false">
|
<div class="student-library">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="header-section" :bordered="false">
|
<div class="header-section" :bordered="false">
|
||||||
<h1 class="page-title">全部学员</h1>
|
<h1 class="page-title">全部学员</h1>
|
||||||
@ -8,8 +8,10 @@
|
|||||||
<n-button type="primary" ghost @click="handleStats">统计分析</n-button>
|
<n-button type="primary" ghost @click="handleStats">统计分析</n-button>
|
||||||
<n-button type="primary" ghost @click="handleExport">导入</n-button>
|
<n-button type="primary" ghost @click="handleExport">导入</n-button>
|
||||||
<n-button type="primary" ghost @click="handleImport">导出</n-button>
|
<n-button type="primary" ghost @click="handleImport">导出</n-button>
|
||||||
<n-button type="error" ghost @click="handleBatchDelete">删除</n-button>
|
<n-button type="error" ghost @click="handleBatchDelete" :disabled="checkedRowKeys.length === 0">
|
||||||
<n-input v-model:value="searchKeyword" placeholder="请输入人员姓名学号" style="width: 200px;"
|
删除({{ checkedRowKeys.length }})
|
||||||
|
</n-button>
|
||||||
|
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px;"
|
||||||
@keyup.enter="handleSearch">
|
@keyup.enter="handleSearch">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-button text @click="handleSearch">
|
<n-button text @click="handleSearch">
|
||||||
@ -27,23 +29,104 @@
|
|||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<div class="table-card" :bordered="false">
|
<div class="table-card" :bordered="false">
|
||||||
<n-data-table :columns="columns" :data="filteredStudentList" :loading="loading"
|
<n-data-table :columns="columns" :data="studentList" :loading="loading" :row-key="rowKey"
|
||||||
:pagination="paginationReactive" :row-key="rowKey" :checked-row-keys="checkedRowKeys"
|
:checked-row-keys="checkedRowKeys" @update:checked-row-keys="handleCheck" striped size="medium" />
|
||||||
@update:checked-row-keys="handleCheck" striped size="medium" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="student-library" v-else>
|
<!-- 独立的分页器 -->
|
||||||
<!-- TODO: 暂时传id为1,来显示模拟数据,对接接口需要去掉 -->
|
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
||||||
<ClassManagement type="student" :class-id="1"></ClassManagement>
|
<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>
|
||||||
|
|
||||||
|
<!-- 添加学员弹窗 -->
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, h, computed } from 'vue'
|
import { ref, reactive, onMounted, h, computed, watch, nextTick } from 'vue'
|
||||||
import { NButton, useMessage, useDialog } from 'naive-ui'
|
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 { 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 message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@ -55,153 +138,152 @@ const searchKeyword = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const checkedRowKeys = ref<Array<string | number>>([])
|
const checkedRowKeys = ref<Array<string | number>>([])
|
||||||
|
|
||||||
// 模拟学员数据
|
// 学员数据
|
||||||
const studentList = ref([
|
const studentList = ref<StudentItem[]>([])
|
||||||
{
|
const classList = ref<ClassItem[]>([])
|
||||||
id: 1,
|
|
||||||
sequence: 1,
|
// 总学员数
|
||||||
name: '王琪琨',
|
const totalStudents = ref(0)
|
||||||
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 paginationReactive = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
showSizePicker: true,
|
total: 0
|
||||||
pageSizes: [10, 20, 50],
|
|
||||||
onChange: (page: number) => {
|
|
||||||
paginationReactive.page = page
|
|
||||||
},
|
|
||||||
onUpdatePageSize: (pageSize: number) => {
|
|
||||||
paginationReactive.pageSize = pageSize
|
|
||||||
paginationReactive.page = 1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 过滤后的学员列表
|
// 强制更新分页器的key
|
||||||
const filteredStudentList = computed(() => {
|
const paginationKey = ref(0)
|
||||||
if (!searchKeyword.value) {
|
|
||||||
return studentList.value
|
// 监听pagination.total的变化,强制更新分页器
|
||||||
}
|
watch(() => pagination.total, () => {
|
||||||
return studentList.value.filter(student =>
|
nextTick(() => {
|
||||||
student.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
paginationKey.value++
|
||||||
student.studentId.includes(searchKeyword.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
|
// 表格行key
|
||||||
const rowKey = (row: any) => row.id
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
type: 'selection'
|
type: 'selection'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
key: 'sequence',
|
|
||||||
width: 80,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '姓名',
|
title: '姓名',
|
||||||
key: 'name',
|
key: 'studentName',
|
||||||
width: 100,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '学号',
|
|
||||||
key: 'studentId',
|
|
||||||
width: 140,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '性别',
|
|
||||||
key: 'gender',
|
|
||||||
width: 80,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '所属学校',
|
|
||||||
key: 'school',
|
|
||||||
width: 120,
|
width: 120,
|
||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '所属班级',
|
title: '账号',
|
||||||
key: 'class',
|
key: 'accountNumber',
|
||||||
width: 300,
|
width: 140,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '班级',
|
||||||
|
key: 'className',
|
||||||
|
width: 150,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
ellipsis: {
|
render: (row: StudentItem) => {
|
||||||
tooltip: true
|
// 使用辅助函数获取班级名称数组
|
||||||
|
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: '加入时间',
|
title: '加入时间',
|
||||||
key: 'joinTime',
|
key: 'joinTime',
|
||||||
width: 160,
|
width: 140,
|
||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -209,51 +291,272 @@ const columns = [
|
|||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 200,
|
width: 200,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render(row: any) {
|
render: (row: StudentItem) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, [
|
return h(
|
||||||
h(
|
NSpace,
|
||||||
NButton,
|
{ size: 'small', justify: 'center' },
|
||||||
{
|
{
|
||||||
size: 'small',
|
default: () => [
|
||||||
type: 'info',
|
h(
|
||||||
ghost: true,
|
NButton,
|
||||||
onClick: () => handleViewProgress(row)
|
{
|
||||||
},
|
size: 'small',
|
||||||
{ default: () => '学习进度' }
|
type: 'info',
|
||||||
),
|
ghost: true,
|
||||||
h(
|
onClick: () => handleViewProgress(row)
|
||||||
NButton,
|
},
|
||||||
{
|
{ default: () => '学习进度' }
|
||||||
size: 'small',
|
),
|
||||||
type: 'primary',
|
h(
|
||||||
ghost: true,
|
NButton,
|
||||||
onClick: () => handleEditStudent(row)
|
{
|
||||||
},
|
size: 'small',
|
||||||
{ default: () => '编辑' }
|
type: 'primary',
|
||||||
),
|
ghost: true,
|
||||||
h(
|
onClick: () => handleEditStudent(row)
|
||||||
NButton,
|
},
|
||||||
{
|
{ default: () => '编辑' }
|
||||||
size: 'small',
|
),
|
||||||
type: 'error',
|
h(
|
||||||
ghost: true,
|
NButton,
|
||||||
onClick: () => handleDeleteStudent(row)
|
{
|
||||||
},
|
size: 'small',
|
||||||
{ default: () => '删除' }
|
type: 'error',
|
||||||
)
|
ghost: true,
|
||||||
])
|
onClick: () => handleDeleteStudent(row)
|
||||||
|
},
|
||||||
|
{ default: () => '删除' }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 方法实现
|
// 加载班级列表数据
|
||||||
const handleSearch = () => {
|
const loadClassList = async () => {
|
||||||
// 搜索逻辑已通过computed实现
|
try {
|
||||||
message.success('搜索完成')
|
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 = () => {
|
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 = () => {
|
const handleStats = () => {
|
||||||
@ -279,13 +582,33 @@ const handleBatchDelete = () => {
|
|||||||
content: `确定要删除选中的 ${checkedRowKeys.value.length} 名学员吗?`,
|
content: `确定要删除选中的 ${checkedRowKeys.value.length} 名学员吗?`,
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: async () => {
|
||||||
// 模拟删除操作
|
try {
|
||||||
studentList.value = studentList.value.filter(student =>
|
// 获取选中的学员
|
||||||
!checkedRowKeys.value.includes(student.id)
|
const selectedStudents = studentList.value.filter((student: StudentItem) =>
|
||||||
)
|
checkedRowKeys.value.includes(student.id)
|
||||||
checkedRowKeys.value = []
|
)
|
||||||
message.success('删除成功')
|
|
||||||
|
// 逐个删除学员
|
||||||
|
for (const student of selectedStudents) {
|
||||||
|
// 找到学员所在的班级ID
|
||||||
|
const classId = student.className
|
||||||
|
if (classId) {
|
||||||
|
await ClassApi.removeStudent(classId, student.accountNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(`成功删除 ${selectedStudents.length} 名学员`)
|
||||||
|
|
||||||
|
// 清空选中状态
|
||||||
|
checkedRowKeys.value = []
|
||||||
|
|
||||||
|
// 重新加载数据
|
||||||
|
loadAllStudents()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
message.error('批量删除失败,请重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -294,33 +617,59 @@ const handleCheck = (keys: Array<string | number>) => {
|
|||||||
checkedRowKeys.value = keys
|
checkedRowKeys.value = keys
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleViewProgress = (row: any) => {
|
const handleViewProgress = (row: StudentItem) => {
|
||||||
message.info(`查看 ${row.name} 的学习进度`)
|
message.info(`查看 ${row.studentName} 的学习进度`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditStudent = (row: any) => {
|
const handleEditStudent = (row: StudentItem) => {
|
||||||
message.info(`编辑学员 ${row.name}`)
|
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({
|
dialog.warning({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: `确定要删除学员 ${row.name} 吗?`,
|
content: `确定要删除学员 ${row.studentName} 吗?`,
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: async () => {
|
||||||
const index = studentList.value.findIndex(student => student.id === row.id)
|
try {
|
||||||
if (index > -1) {
|
// 找到学员所在的班级ID
|
||||||
studentList.value.splice(index, 1)
|
const classId = row.className
|
||||||
message.success('删除成功')
|
if (!classId) {
|
||||||
|
message.error('无法确定学员所在班级,删除失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用删除接口
|
||||||
|
await ClassApi.removeStudent(classId, row.accountNumber)
|
||||||
|
|
||||||
|
message.success(`已删除学员:${row.studentName}`)
|
||||||
|
|
||||||
|
// 重新加载数据
|
||||||
|
loadAllStudents()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除学员失败:', error)
|
||||||
|
message.error('删除失败,请重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化数据
|
// 加载学校列表
|
||||||
loading.value = false
|
loadSchoolList()
|
||||||
|
// 加载所有班级的学员数据
|
||||||
|
loadAllStudents()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -360,4 +709,37 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
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>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user