feat:添加学院中心下班级管理页面和班级管理下的统计分析、学习进度页面
BIN
public/analysis/icon/分组 106.png
Normal file
After Width: | Height: | Size: 625 B |
BIN
public/analysis/icon/分组 86.png
Normal file
After Width: | Height: | Size: 806 B |
BIN
public/analysis/icon/分组 93.png
Normal file
After Width: | Height: | Size: 821 B |
BIN
public/analysis/icon/切片 21.png
Normal file
After Width: | Height: | Size: 562 B |
BIN
public/analysis/icon/路径 10.png
Normal file
After Width: | Height: | Size: 775 B |
BIN
public/analysis/icon/路径 11.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
public/analysis/icon/路径 18.png
Normal file
After Width: | Height: | Size: 917 B |
BIN
public/analysis/icon/路径 19.png
Normal file
After Width: | Height: | Size: 834 B |
BIN
public/analysis/切片 28.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/analysis/切片 29.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/analysis/切片 30.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/analysis/切片 31.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/analysis/切片 32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/analysis/切片 33.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/analysis/切片 34.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/analysis/切片 35.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
@ -38,7 +38,7 @@
|
|||||||
</template>
|
</template>
|
||||||
管理班级
|
管理班级
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button type="primary" ghost @click="showInviteModal = true">
|
<n-button type="primary" ghost @click="openInviteModal(selectedDepartment || props.classId?.toString() || '1')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<QrCode />
|
<QrCode />
|
||||||
@ -65,7 +65,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'">
|
<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">
|
||||||
@ -145,50 +145,6 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
<!-- 删除确认弹窗 -->
|
|
||||||
<n-modal v-model:show="showDeleteConfirmModal" title="确认删除">
|
|
||||||
<n-card style="width: 400px" title="确认删除" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
|
||||||
<div class="delete-confirm-content">
|
|
||||||
<p>确定要删除班级“{{ currentDeleteClass?.className }}”吗?</p>
|
|
||||||
<p class="delete-warning">删除后将无法恢复!</p>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<n-button @click="showDeleteConfirmModal = false">取消</n-button>
|
|
||||||
<n-button type="error" @click="confirmDeleteClass">确认删除</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-card>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
<!-- 批量删除确认弹窗 -->
|
|
||||||
<n-modal v-model:show="showBatchDeleteModal" title="批量移除确认">
|
|
||||||
<n-card style="width: 450px" title="批量移除确认" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
|
||||||
<div class="batch-delete-content">
|
|
||||||
<p>确定要移除选中的 <strong>{{ selectedRowKeys.length }}</strong> 名学员吗?</p>
|
|
||||||
<div class="selected-students">
|
|
||||||
<div class="student-list">
|
|
||||||
<div
|
|
||||||
v-for="student in selectedStudents"
|
|
||||||
:key="student.id"
|
|
||||||
class="student-item"
|
|
||||||
>
|
|
||||||
<span class="student-name">{{ student.studentName }}</span>
|
|
||||||
<span class="student-account">({{ student.accountNumber }})</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="batch-warning">移除后这些学员将无法访问班级资源!</p>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<n-button @click="showBatchDeleteModal = false">取消</n-button>
|
|
||||||
<n-button type="error" @click="confirmBatchDelete">确认移除</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-card>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
<!-- 批量调班确认弹窗 -->
|
<!-- 批量调班确认弹窗 -->
|
||||||
<n-modal v-model:show="showBatchTransferModal" title="批量调班">
|
<n-modal v-model:show="showBatchTransferModal" title="批量调班">
|
||||||
<n-card style="width: 550px" title="批量调班" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
<n-card style="width: 550px" title="批量调班" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
||||||
@ -329,7 +285,10 @@
|
|||||||
<n-card style="width: 400px" title="邀请码" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
<n-card style="width: 400px" title="邀请码" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
||||||
<div class="invite-content">
|
<div class="invite-content">
|
||||||
<div class="invite-code-display">
|
<div class="invite-code-display">
|
||||||
<div class="invite-title">邀请码</div>
|
<div class="invite-title">班级邀请码</div>
|
||||||
|
<div class="invite-note" v-if="currentInviteClassId">
|
||||||
|
班级:{{ masterClassList.find(item => item.id === currentInviteClassId)?.className || '未知班级' }}
|
||||||
|
</div>
|
||||||
<div class="invite-code">{{ inviteCode }}</div>
|
<div class="invite-code">{{ inviteCode }}</div>
|
||||||
<n-button ghost type="primary" @click="copyInviteCode">复制</n-button>
|
<n-button ghost type="primary" @click="copyInviteCode">复制</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -354,7 +313,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, h, computed } from 'vue'
|
import { ref, onMounted, h, computed, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { AddCircleOutline, SettingsOutline, QrCode } from '@vicons/ionicons5'
|
import { AddCircleOutline, SettingsOutline, QrCode } from '@vicons/ionicons5'
|
||||||
import {
|
import {
|
||||||
NDataTable,
|
NDataTable,
|
||||||
@ -371,6 +331,7 @@ import {
|
|||||||
NCheckbox,
|
NCheckbox,
|
||||||
NDropdown,
|
NDropdown,
|
||||||
useMessage,
|
useMessage,
|
||||||
|
useDialog,
|
||||||
type FormInst,
|
type FormInst,
|
||||||
type FormRules
|
type FormRules
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
@ -380,11 +341,13 @@ import ImportModal from '@/components/common/ImportModal.vue'
|
|||||||
// 定义 props 类型
|
// 定义 props 类型
|
||||||
interface Props {
|
interface Props {
|
||||||
type: 'course' | 'student'
|
type: 'course' | 'student'
|
||||||
|
classId?: number | null // 新增班级ID参数
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收 props
|
// 接收 props
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: 'student'
|
type: 'student',
|
||||||
|
classId: null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义数据类型
|
// 定义数据类型
|
||||||
@ -418,8 +381,11 @@ interface FormData {
|
|||||||
|
|
||||||
const totalStudents = ref(1333)
|
const totalStudents = ref(1333)
|
||||||
const inviteCode = ref('56685222')
|
const inviteCode = ref('56685222')
|
||||||
|
const currentInviteClassId = ref<string | null>(null) // 当前邀请码对应的班级ID
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const selectedDepartment = ref('')
|
const selectedDepartment = ref('')
|
||||||
@ -429,7 +395,6 @@ const showInviteModal = ref(false)
|
|||||||
const showTransferModal = ref(false)
|
const showTransferModal = ref(false)
|
||||||
const showAddClassModal = ref(false)
|
const showAddClassModal = ref(false)
|
||||||
const showManageClassModal = ref(false)
|
const showManageClassModal = ref(false)
|
||||||
const showDeleteConfirmModal = ref(false)
|
|
||||||
const showImportModal = ref(false)
|
const showImportModal = ref(false)
|
||||||
const selectedTargetClass = ref('')
|
const selectedTargetClass = ref('')
|
||||||
const currentTransferStudent = ref<StudentItem | null>(null)
|
const currentTransferStudent = ref<StudentItem | null>(null)
|
||||||
@ -438,8 +403,6 @@ const classFormRef = ref<FormInst | null>(null)
|
|||||||
const isEditMode = ref(false)
|
const isEditMode = ref(false)
|
||||||
const currentEditId = ref('')
|
const currentEditId = ref('')
|
||||||
const isRenameMode = ref(false)
|
const isRenameMode = ref(false)
|
||||||
const currentDeleteClass = ref<any>(null)
|
|
||||||
const showBatchDeleteModal = ref(false)
|
|
||||||
const showBatchTransferModal = ref(false)
|
const showBatchTransferModal = ref(false)
|
||||||
const selectedRowKeys = ref<string[]>([]) // 多选行的keys
|
const selectedRowKeys = ref<string[]>([]) // 多选行的keys
|
||||||
|
|
||||||
@ -466,7 +429,7 @@ const rules: FormRules = {
|
|||||||
{ required: true, message: '请输入学员学号', trigger: 'blur' }
|
{ required: true, message: '请输入学员学号', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
loginPassword: [
|
loginPassword: [
|
||||||
{ required: true, message: '请输入登录密码', trigger: 'blur' },
|
// { required: true, message: '请输入登录密码', trigger: 'blur' },
|
||||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
college: [
|
college: [
|
||||||
@ -728,12 +691,12 @@ const pagination = ref({
|
|||||||
pageSizes: [10, 20, 50],
|
pageSizes: [10, 20, 50],
|
||||||
onChange: (page: number) => {
|
onChange: (page: number) => {
|
||||||
pagination.value.page = page
|
pagination.value.page = page
|
||||||
loadData()
|
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()
|
loadData(props.classId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -742,6 +705,11 @@ const handleTransfer = (row: StudentItem) => {
|
|||||||
currentTransferStudent.value = row
|
currentTransferStudent.value = row
|
||||||
selectedTargetClass.value = ''
|
selectedTargetClass.value = ''
|
||||||
showTransferModal.value = true
|
showTransferModal.value = true
|
||||||
|
|
||||||
|
console.log('打开调班弹窗:', {
|
||||||
|
学员信息: row,
|
||||||
|
可选班级: classOptions.value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = (row: StudentItem) => {
|
const handleEdit = (row: StudentItem) => {
|
||||||
@ -764,7 +732,37 @@ const handleBatchDelete = () => {
|
|||||||
message.warning('请先选择要移除的学员')
|
message.warning('请先选择要移除的学员')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showBatchDeleteModal.value = true
|
|
||||||
|
const selectedStudentsList = selectedStudents.value
|
||||||
|
const studentNames = selectedStudentsList.map(s => s.studentName).join('、')
|
||||||
|
|
||||||
|
dialog.info({
|
||||||
|
title: '批量移除确认',
|
||||||
|
content: `确定要移除选中的 ${selectedRowKeys.value.length} 名学员吗?\n\n学员名单:${studentNames}\n\n移除后这些学员将无法访问班级资源!`,
|
||||||
|
positiveText: '确认移除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
// 这里模拟 API 调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
const removedCount = selectedRowKeys.value.length
|
||||||
|
|
||||||
|
// 从数据中移除选中的学员
|
||||||
|
data.value = data.value.filter(student => !selectedRowKeys.value.includes(student.id))
|
||||||
|
|
||||||
|
// 清空选中状态
|
||||||
|
selectedRowKeys.value = []
|
||||||
|
|
||||||
|
message.success(`成功移除 ${removedCount} 名学员`)
|
||||||
|
|
||||||
|
// 重新加载数据
|
||||||
|
loadData(props.classId)
|
||||||
|
} catch (error) {
|
||||||
|
message.error('批量移除失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量调班处理
|
// 批量调班处理
|
||||||
@ -777,32 +775,6 @@ const handleBatchTransfer = () => {
|
|||||||
showBatchTransferModal.value = true
|
showBatchTransferModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认批量删除
|
|
||||||
const confirmBatchDelete = async () => {
|
|
||||||
try {
|
|
||||||
// 这里模拟 API 调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
const removedCount = selectedRowKeys.value.length
|
|
||||||
|
|
||||||
// 从数据中移除选中的学员
|
|
||||||
data.value = data.value.filter(student => !selectedRowKeys.value.includes(student.id))
|
|
||||||
|
|
||||||
// 清空选中状态
|
|
||||||
selectedRowKeys.value = []
|
|
||||||
|
|
||||||
// 关闭弹窗
|
|
||||||
showBatchDeleteModal.value = false
|
|
||||||
|
|
||||||
message.success(`成功移除 ${removedCount} 名学员`)
|
|
||||||
|
|
||||||
// 重新加载数据
|
|
||||||
loadData()
|
|
||||||
} catch (error) {
|
|
||||||
message.error('批量移除失败,请重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认批量调班
|
// 确认批量调班
|
||||||
const confirmBatchTransfer = async () => {
|
const confirmBatchTransfer = async () => {
|
||||||
if (!selectedTargetClass.value) {
|
if (!selectedTargetClass.value) {
|
||||||
@ -830,7 +802,7 @@ const confirmBatchTransfer = async () => {
|
|||||||
selectedRowKeys.value = []
|
selectedRowKeys.value = []
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
loadData()
|
loadData(props.classId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('批量调班失败,请重试')
|
message.error('批量调班失败,请重试')
|
||||||
}
|
}
|
||||||
@ -842,16 +814,56 @@ const handleDelete = (row: StudentItem) => {
|
|||||||
|
|
||||||
// 查看学习进度处理函数(student 模式)
|
// 查看学习进度处理函数(student 模式)
|
||||||
const handleViewProgress = (row: StudentItem) => {
|
const handleViewProgress = (row: StudentItem) => {
|
||||||
message.info(`查看 ${row.studentName} 的学习进度`)
|
// 跳转到学习进度页面,传递学员信息作为查询参数
|
||||||
// TODO: 实现查看学习进度的具体逻辑
|
router.push({
|
||||||
console.log('查看学习进度:', row)
|
name: 'StudentProgress',
|
||||||
|
query: {
|
||||||
|
studentId: row.id,
|
||||||
|
studentName: row.studentName,
|
||||||
|
accountNumber: row.accountNumber,
|
||||||
|
className: row.className
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计分析处理函数(student 模式)
|
||||||
|
const handleStatisticsAnalysis = () => {
|
||||||
|
// 跳转到统计分析页面
|
||||||
|
router.push({
|
||||||
|
name: 'StatisticsAnalysis'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除学员处理函数(student 模式)
|
// 删除学员处理函数(student 模式)
|
||||||
const handleDeleteStudent = (row: StudentItem) => {
|
const handleDeleteStudent = (row: StudentItem) => {
|
||||||
message.warning(`删除学员:${row.studentName}`)
|
dialog.info({
|
||||||
// TODO: 实现删除学员的具体逻辑
|
title: '确认删除',
|
||||||
console.log('删除学员:', row)
|
content: `确定要删除学员"${row.studentName}"吗?\n\n删除后将无法恢复!`,
|
||||||
|
positiveText: '确认删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
// 这里模拟 API 调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
const studentName = row.studentName
|
||||||
|
const studentId = row.id
|
||||||
|
|
||||||
|
// 从数据中移除学员
|
||||||
|
data.value = data.value.filter(student => student.id !== studentId)
|
||||||
|
|
||||||
|
// 更新学员总数
|
||||||
|
totalStudents.value = data.value.length
|
||||||
|
|
||||||
|
message.success(`已删除学员:${studentName}`)
|
||||||
|
|
||||||
|
// 重新加载数据以确保数据同步
|
||||||
|
loadData(props.classId)
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmTransfer = async () => {
|
const handleConfirmTransfer = async () => {
|
||||||
@ -884,7 +896,7 @@ const handleConfirmTransfer = async () => {
|
|||||||
selectedTargetClass.value = ''
|
selectedTargetClass.value = ''
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
loadData()
|
loadData(props.classId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('调班失败,请重试')
|
message.error('调班失败,请重试')
|
||||||
}
|
}
|
||||||
@ -892,14 +904,46 @@ const handleConfirmTransfer = async () => {
|
|||||||
|
|
||||||
// 判断是否为当前班级
|
// 判断是否为当前班级
|
||||||
const isCurrentClass = (classValue: string) => {
|
const isCurrentClass = (classValue: string) => {
|
||||||
if (!currentTransferStudent.value) return false
|
if (!currentTransferStudent.value) {
|
||||||
|
console.log('调班判断: 未选中学员')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 根据学员的className匹配班级value
|
// 根据学员的className匹配班级value
|
||||||
const studentClassName = currentTransferStudent.value.className
|
const studentClassName = currentTransferStudent.value.className
|
||||||
const classOption = masterClassList.value.find(item => item.className === studentClassName)
|
const classOption = masterClassList.value.find(item => item.className === studentClassName)
|
||||||
|
|
||||||
|
console.log('调班判断:', {
|
||||||
|
学员姓名: currentTransferStudent.value.studentName,
|
||||||
|
学员班级: studentClassName,
|
||||||
|
待匹配班级ID: classValue,
|
||||||
|
找到的班级: classOption,
|
||||||
|
是否匹配: classOption?.id === classValue
|
||||||
|
})
|
||||||
|
|
||||||
return classOption?.id === classValue
|
return classOption?.id === classValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据班级ID生成邀请码
|
||||||
|
const generateInviteCode = (classId: string) => {
|
||||||
|
// 模拟根据班级ID生成不同的邀请码
|
||||||
|
const baseCode = 56685222
|
||||||
|
const numericClassId = parseInt(classId) || 1
|
||||||
|
return (baseCode + numericClassId * 1000).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开邀请码弹窗
|
||||||
|
const openInviteModal = (classId: string) => {
|
||||||
|
currentInviteClassId.value = classId
|
||||||
|
inviteCode.value = generateInviteCode(classId)
|
||||||
|
showInviteModal.value = true
|
||||||
|
|
||||||
|
console.log('打开邀请码弹窗:', {
|
||||||
|
班级ID: classId,
|
||||||
|
邀请码: inviteCode.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const copyInviteCode = () => {
|
const copyInviteCode = () => {
|
||||||
navigator.clipboard.writeText(inviteCode.value).then(() => {
|
navigator.clipboard.writeText(inviteCode.value).then(() => {
|
||||||
message.success('邀请码已复制到剪贴板')
|
message.success('邀请码已复制到剪贴板')
|
||||||
@ -925,7 +969,7 @@ const handleSubmit = async () => {
|
|||||||
resetForm()
|
resetForm()
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
loadData()
|
loadData(props.classId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('请检查表单信息')
|
message.error('请检查表单信息')
|
||||||
}
|
}
|
||||||
@ -1040,52 +1084,120 @@ const handleRenameClass = (classItem: any) => {
|
|||||||
|
|
||||||
// 删除班级确认
|
// 删除班级确认
|
||||||
const handleDeleteClass = (classItem: any) => {
|
const handleDeleteClass = (classItem: any) => {
|
||||||
currentDeleteClass.value = classItem
|
dialog.info({
|
||||||
showDeleteConfirmModal.value = true
|
title: '确认删除',
|
||||||
}
|
content: `确定要删除班级"${classItem.className}"吗?\n\n删除后将无法恢复!`,
|
||||||
|
positiveText: '确认删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
// 这里模拟 API 调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
// 确认删除班级
|
message.success(`已删除班级:${classItem.className}`)
|
||||||
const confirmDeleteClass = async () => {
|
|
||||||
try {
|
|
||||||
// 这里模拟 API 调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
message.success(`已删除班级:${currentDeleteClass.value.className}`)
|
// 从主数据源中移除
|
||||||
|
const index = masterClassList.value.findIndex(item => item.id === classItem.id)
|
||||||
|
if (index > -1) {
|
||||||
|
masterClassList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
// 从主数据源中移除
|
} catch (error) {
|
||||||
const index = masterClassList.value.findIndex(item => item.id === currentDeleteClass.value.id)
|
message.error('删除失败,请重试')
|
||||||
if (index > -1) {
|
}
|
||||||
masterClassList.value.splice(index, 1)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// 关闭确认弹窗
|
|
||||||
showDeleteConfirmModal.value = false
|
|
||||||
currentDeleteClass.value = null
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
message.error('删除失败,请重试')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟数据加载
|
// 模拟数据加载
|
||||||
const loadData = async () => {
|
const loadData = async (classId?: number | null) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
const mockData: StudentItem[] = Array.from({ length: 3 }, (_, index) => ({
|
// 根据班级ID模拟不同的数据
|
||||||
id: `student_${index + 1}`,
|
let mockData: StudentItem[] = []
|
||||||
studentName: ['张华', '李明', '王丽'][index],
|
|
||||||
accountNumber: [`${(1660340 + index + 1).toString()}`, `${(1660340 + index + 2).toString()}`, `${(1660340 + index + 3).toString()}`][index],
|
if (classId === null || classId === undefined) {
|
||||||
className: '计算机1',
|
// 未选择班级时显示空数据或默认数据
|
||||||
college: '清华大学经管学院',
|
mockData = []
|
||||||
loginName: [`${(1660340 + index + 1).toString()}`, `${(1660340 + index + 2).toString()}`, `${(1660340 + index + 3).toString()}`][index],
|
} else {
|
||||||
joinTime: '2025.07.25 08:20'
|
// 根据不同班级ID返回不同的模拟数据
|
||||||
}))
|
const classDataMap: Record<number, StudentItem[]> = {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
id: 'student_1_1',
|
||||||
|
studentName: '张华',
|
||||||
|
accountNumber: '1660341',
|
||||||
|
className: '软件工程1班',
|
||||||
|
college: '计算机学院',
|
||||||
|
loginName: '1660341',
|
||||||
|
joinTime: '2025.07.25 08:20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'student_1_2',
|
||||||
|
studentName: '李明',
|
||||||
|
accountNumber: '1660342',
|
||||||
|
className: '软件工程1班',
|
||||||
|
college: '计算机学院',
|
||||||
|
loginName: '1660342',
|
||||||
|
joinTime: '2025.07.25 09:15'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{
|
||||||
|
id: 'student_2_1',
|
||||||
|
studentName: '王丽',
|
||||||
|
accountNumber: '1660343',
|
||||||
|
className: '软件工程2班',
|
||||||
|
college: '软件学院',
|
||||||
|
loginName: '1660343',
|
||||||
|
joinTime: '2025.07.26 10:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'student_2_2',
|
||||||
|
studentName: '赵强',
|
||||||
|
accountNumber: '1660344',
|
||||||
|
className: '软件工程2班',
|
||||||
|
college: '软件学院',
|
||||||
|
loginName: '1660344',
|
||||||
|
joinTime: '2025.07.26 11:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'student_2_3',
|
||||||
|
studentName: '孙美',
|
||||||
|
accountNumber: '1660345',
|
||||||
|
className: '软件工程2班',
|
||||||
|
college: '软件学院',
|
||||||
|
loginName: '1660345',
|
||||||
|
joinTime: '2025.07.26 14:20'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{
|
||||||
|
id: 'student_3_1',
|
||||||
|
studentName: '周杰',
|
||||||
|
accountNumber: '1660346',
|
||||||
|
className: '计算机科学1班',
|
||||||
|
college: '数学学院',
|
||||||
|
loginName: '1660346',
|
||||||
|
joinTime: '2025.07.27 08:50'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
mockData = classDataMap[classId] || []
|
||||||
|
}
|
||||||
|
|
||||||
data.value = mockData
|
data.value = mockData
|
||||||
|
|
||||||
|
// 更新学员总数
|
||||||
|
totalStudents.value = mockData.length
|
||||||
|
|
||||||
|
console.log(`加载班级 ${classId} 的数据,共 ${mockData.length} 名学员`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载数据失败:', error)
|
console.error('加载数据失败:', error)
|
||||||
|
message.error('加载数据失败,请重试')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -1097,7 +1209,7 @@ const handleImportSuccess = (result: any) => {
|
|||||||
message.success(`导入完成!成功:${result.details?.success || 0} 条,失败:${result.details?.failed || 0} 条`)
|
message.success(`导入完成!成功:${result.details?.success || 0} 条,失败:${result.details?.failed || 0} 条`)
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
loadData()
|
loadData(props.classId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模板下载处理
|
// 模板下载处理
|
||||||
@ -1107,8 +1219,46 @@ const handleTemplateDownload = (type?: string) => {
|
|||||||
// TODO: 实现模板下载逻辑
|
// TODO: 实现模板下载逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听班级ID变化,重新加载数据
|
||||||
|
watch(
|
||||||
|
() => props.classId,
|
||||||
|
(newClassId, oldClassId) => {
|
||||||
|
console.log(`班级ID从 ${oldClassId} 变更为 ${newClassId}`)
|
||||||
|
if (newClassId !== oldClassId) {
|
||||||
|
// 同步更新选择器的状态
|
||||||
|
selectedDepartment.value = newClassId ? newClassId.toString() : ''
|
||||||
|
loadData(newClassId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false } // 不立即执行,避免与onMounted重复
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听部门/班级选择器变化,重新加载数据
|
||||||
|
watch(
|
||||||
|
() => selectedDepartment.value,
|
||||||
|
(newDepartmentId, oldDepartmentId) => {
|
||||||
|
console.log(`选择的班级从 ${oldDepartmentId} 变更为 ${newDepartmentId}`)
|
||||||
|
if (newDepartmentId !== oldDepartmentId) {
|
||||||
|
// 当选择器有值时,使用选择器的值;否则使用传入的classId
|
||||||
|
const targetClassId = newDepartmentId ? Number(newDepartmentId) : props.classId
|
||||||
|
loadData(targetClassId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
// 初始加载时,优先使用使用传入的classId,其次使用选择器的值
|
||||||
|
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
|
||||||
|
loadData(initialClassId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
openAddClassModal,
|
||||||
|
handleRenameClass,
|
||||||
|
handleDeleteClass,
|
||||||
|
openInviteModal
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -1118,7 +1268,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
/* margin: 16px 0; */
|
padding-bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1341,7 +1491,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
.batch-delete-content {
|
.batch-delete-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-delete-content p {
|
.batch-delete-content p {
|
||||||
@ -1443,9 +1592,15 @@ onMounted(() => {
|
|||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delete-warning {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.delete-confirm-content {
|
.delete-confirm-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-confirm-content p {
|
.delete-confirm-content p {
|
||||||
@ -1453,16 +1608,6 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-warning {
|
|
||||||
color: #ff4d4f;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 批量调班样式 */
|
|
||||||
.batch-transfer-content {
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-transfer-content p {
|
.batch-transfer-content p {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -66,6 +66,8 @@ import HomeworkTemplateImport from '@/views/teacher/course/HomeworkTemplateImpor
|
|||||||
// 学员管理组件
|
// 学员管理组件
|
||||||
import StudentLibrary from '@/views/teacher/student/StudentLibrary.vue'
|
import StudentLibrary from '@/views/teacher/student/StudentLibrary.vue'
|
||||||
import ClassManagement from '@/views/teacher/student/ClassManagement.vue'
|
import ClassManagement from '@/views/teacher/student/ClassManagement.vue'
|
||||||
|
import StudentProgress from '@/views/teacher/student/StudentProgress.vue'
|
||||||
|
import StatisticsAnalysis from '@/views/teacher/student/StatisticsAnalysis.vue'
|
||||||
import ExamManagement from '@/views/teacher/ExamPages/ExamPage.vue'
|
import ExamManagement from '@/views/teacher/ExamPages/ExamPage.vue'
|
||||||
import ExamQuestionBankManagement from '@/views/teacher/ExamPages/QuestionBankManagement.vue'
|
import ExamQuestionBankManagement from '@/views/teacher/ExamPages/QuestionBankManagement.vue'
|
||||||
import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue'
|
import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue'
|
||||||
@ -310,6 +312,18 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'ClassManagement',
|
name: 'ClassManagement',
|
||||||
component: ClassManagement,
|
component: ClassManagement,
|
||||||
meta: { title: '班级管理' }
|
meta: { title: '班级管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'student-progress',
|
||||||
|
name: 'StudentProgress',
|
||||||
|
component: StudentProgress,
|
||||||
|
meta: { title: '学习进度' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'statistics-analysis',
|
||||||
|
name: 'StatisticsAnalysis',
|
||||||
|
component: StatisticsAnalysis,
|
||||||
|
meta: { title: '统计分析' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,185 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="class-management">
|
||||||
课程管理开发中
|
<div class="class-left">
|
||||||
|
<span class="class-title">班级管理</span>
|
||||||
|
<n-collapse :default-expanded-names="['1']">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-popselect
|
||||||
|
trigger="hover"
|
||||||
|
placement="bottom-start"
|
||||||
|
:options="classMenuOptions"
|
||||||
|
@update:value="handleClassMenuSelect"
|
||||||
|
>
|
||||||
|
<n-icon style="cursor: pointer;">
|
||||||
|
<EllipsisVertical />
|
||||||
|
</n-icon>
|
||||||
|
</n-popselect>
|
||||||
|
</template>
|
||||||
|
<template #arrow>
|
||||||
|
<n-icon>
|
||||||
|
<CaretForward />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
<n-collapse-item title="班级管理" name="1">
|
||||||
|
<div
|
||||||
|
class="class-item"
|
||||||
|
:class="{ active: activeClassId === value.id }"
|
||||||
|
v-for="value in classList"
|
||||||
|
:key="value.id"
|
||||||
|
@click="handleClassClick(value.id)"
|
||||||
|
>
|
||||||
|
<div>{{ value.name }}</div>
|
||||||
|
<n-popselect
|
||||||
|
trigger="hover"
|
||||||
|
placement="bottom-start"
|
||||||
|
:options="getClassItemOptions()"
|
||||||
|
@update:value="(selectedValue: string) => handleClassItemMenuSelect(selectedValue, value.id)"
|
||||||
|
>
|
||||||
|
<n-icon style="cursor: pointer;">
|
||||||
|
<EllipsisVertical />
|
||||||
|
</n-icon>
|
||||||
|
</n-popselect>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</div>
|
||||||
|
<div class="class-right">
|
||||||
|
<ClassManagement ref="classManagementRef" :class-id="activeClassId" :class-name="classList.find(item => item.id === activeClassId)?.name" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import ClassManagement from '@/components/teacher/ClassManagement.vue'
|
||||||
|
import { CaretForward, EllipsisVertical } from '@vicons/ionicons5'
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
const classList = ref([
|
||||||
|
{ id: 1, name: "班级一" },
|
||||||
|
{ id: 2, name: "班级二" },
|
||||||
|
{ id: 3, name: "班级三" },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 当前激活的班级ID
|
||||||
|
const activeClassId = ref<number | null>(1)
|
||||||
|
|
||||||
|
// 引用ClassManagement组件
|
||||||
|
const classManagementRef = ref<InstanceType<typeof ClassManagement> | null>(null)
|
||||||
|
|
||||||
|
// 班级菜单选项
|
||||||
|
const classMenuOptions = [
|
||||||
|
{
|
||||||
|
label: '添加班级',
|
||||||
|
value: 'add-class'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取班级项菜单选项
|
||||||
|
const getClassItemOptions = () => [
|
||||||
|
{
|
||||||
|
label: '编辑名称',
|
||||||
|
value: 'edit-name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '邀请码',
|
||||||
|
value: 'invite-code'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
value: 'delete'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 点击班级项的处理函数
|
||||||
|
const handleClassClick = (classId: number) => {
|
||||||
|
activeClassId.value = classId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理班级菜单选择
|
||||||
|
const handleClassMenuSelect = (value: string) => {
|
||||||
|
if (value === 'add-class') {
|
||||||
|
// 调用ClassManagement组件内的添加班级弹窗
|
||||||
|
if (classManagementRef.value) {
|
||||||
|
classManagementRef.value.openAddClassModal?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理班级项菜单选择
|
||||||
|
const handleClassItemMenuSelect = (value: string, classId: number) => {
|
||||||
|
if (!classManagementRef.value) return
|
||||||
|
|
||||||
|
const selectedClass = classList.value.find(item => item.id === classId)
|
||||||
|
if (!selectedClass) return
|
||||||
|
|
||||||
|
switch (value) {
|
||||||
|
case 'edit-name':
|
||||||
|
// 调用组件内部管理班级下的重命名方法
|
||||||
|
classManagementRef.value.handleRenameClass?.({
|
||||||
|
id: classId.toString(),
|
||||||
|
className: selectedClass.name,
|
||||||
|
studentCount: 0,
|
||||||
|
creator: '王建国',
|
||||||
|
createTime: '2025.09.02 09:11'
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'invite-code':
|
||||||
|
// 调用组件内部邀请码弹窗
|
||||||
|
classManagementRef.value.openInviteModal?.(classId.toString())
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
// 调用组件内部管理班级下的删除方法
|
||||||
|
classManagementRef.value.handleDeleteClass?.({
|
||||||
|
id: classId.toString(),
|
||||||
|
className: selectedClass.name,
|
||||||
|
studentCount: 0,
|
||||||
|
creator: '王建国',
|
||||||
|
createTime: '2025.09.02 09:11'
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.class-management {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
min-height: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-left {
|
||||||
|
width: 240px;
|
||||||
|
padding-right: 16px;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-right {
|
||||||
|
padding: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-item{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 13px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active{
|
||||||
|
background-color: #F5F9FC;
|
||||||
|
color: #0288D1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
1751
src/views/teacher/student/StatisticsAnalysis.vue
Normal file
@ -33,7 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="student-library" v-else>
|
<div class="student-library" v-else>
|
||||||
<ClassManagement type="student"></ClassManagement>
|
<!-- TODO: 暂时传id为1,来显示模拟数据,对接接口需要去掉 -->
|
||||||
|
<ClassManagement type="student" :class-id="1"></ClassManagement>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
645
src/views/teacher/student/StudentProgress.vue
Normal file
@ -0,0 +1,645 @@
|
|||||||
|
<template>
|
||||||
|
<div class="student-progress">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<h1 class="page-title">{{ studentInfo.name }}的学习进度明细</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- <span class="student-info">学号:{{ studentInfo.studentId }} | 班级:{{ studentInfo.className }}</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计图表区域 -->
|
||||||
|
<div class="statistics-section">
|
||||||
|
<!-- 图表容器 -->
|
||||||
|
<v-chart class="chart" :option="chartOption" autoresize />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 课程进度详情 -->
|
||||||
|
<div class="course-progress-section">
|
||||||
|
<n-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="header-course-name">课程名称</div>
|
||||||
|
<div class="header-items">
|
||||||
|
<span class="header-duration">总时长</span>
|
||||||
|
<span class="header-status">课程进度</span>
|
||||||
|
<span class="header-start-time">第一次学习时间</span>
|
||||||
|
<span class="header-complete-time">学习完成时间</span>
|
||||||
|
<span class="header-progress">学习进度</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 课程进度列表 -->
|
||||||
|
<div class="course-list">
|
||||||
|
<div v-for="course in courseProgressData" :key="course.id" class="course-item">
|
||||||
|
<div class="course-info">
|
||||||
|
<div class="course-thumbnail">
|
||||||
|
<n-image width="60" src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" />
|
||||||
|
</div>
|
||||||
|
<div class="course-details">
|
||||||
|
<h4 class="course-title">{{ course.courseName }}</h4>
|
||||||
|
<!-- <p class="course-subtitle">提升您的编程思维和算法能力,高效率学习指南</p> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="course-stats">
|
||||||
|
<span class="duration">{{ Math.floor(course.studyTime) }}小时{{ Math.floor((course.studyTime %
|
||||||
|
1) *
|
||||||
|
60) }}分钟</span>
|
||||||
|
<span class="status">{{ course.progress === 100 ? '已完成课时' : '已完成课时' }} {{
|
||||||
|
course.completedLessons
|
||||||
|
}}/{{ course.totalLessons }}</span>
|
||||||
|
<span class="start-time">{{ course.progress > 0 ? '2022.04.01 20:22' : '-' }}</span>
|
||||||
|
<span class="complete-time">{{ course.progress === 100 ? '2022.04.18 14:00' : '-' }}</span>
|
||||||
|
<div class="progress-bar-wrapper">
|
||||||
|
<n-progress :percentage="course.progress" :show-indicator="false" :height="6"
|
||||||
|
:color="course.progress === 100 ? '#1890ff' : '#1890ff'" />
|
||||||
|
<span class="progress-text">进度{{ course.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||||
|
import {
|
||||||
|
NCard,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
NProgress,
|
||||||
|
NImage
|
||||||
|
} from 'naive-ui'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
import {
|
||||||
|
CanvasRenderer
|
||||||
|
} from 'echarts/renderers'
|
||||||
|
import {
|
||||||
|
LineChart
|
||||||
|
} from 'echarts/charts'
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent
|
||||||
|
} from 'echarts/components'
|
||||||
|
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
LineChart,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent
|
||||||
|
])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 学员信息
|
||||||
|
const studentInfo = ref({
|
||||||
|
id: route.query.studentId as string || '1',
|
||||||
|
name: route.query.studentName as string || '张三',
|
||||||
|
studentId: route.query.accountNumber as string || 'ST001',
|
||||||
|
className: route.query.className as string || '软件工程1班'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟课程进度数据
|
||||||
|
const courseProgressData = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
courseName: 'Python程序设计基础',
|
||||||
|
courseType: '必修课',
|
||||||
|
progress: 100,
|
||||||
|
completedLessons: 15,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 28.5,
|
||||||
|
lastStudyTime: '2024-09-03 14:30',
|
||||||
|
homeworkProgress: 100,
|
||||||
|
examProgress: 85
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseName: '数据结构与算法',
|
||||||
|
courseType: '必修课',
|
||||||
|
progress: 87,
|
||||||
|
completedLessons: 13,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 32.2,
|
||||||
|
lastStudyTime: '2024-09-03 10:15',
|
||||||
|
homeworkProgress: 80,
|
||||||
|
examProgress: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
courseName: 'Web前端开发技术',
|
||||||
|
courseType: '选修课',
|
||||||
|
progress: 73,
|
||||||
|
completedLessons: 11,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 25.8,
|
||||||
|
lastStudyTime: '2024-09-02 16:45',
|
||||||
|
homeworkProgress: 75,
|
||||||
|
examProgress: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
courseName: '数据库系统原理',
|
||||||
|
courseType: '必修课',
|
||||||
|
progress: 60,
|
||||||
|
completedLessons: 9,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 18.5,
|
||||||
|
lastStudyTime: '2024-09-01 09:20',
|
||||||
|
homeworkProgress: 60,
|
||||||
|
examProgress: 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
courseName: '软件工程导论',
|
||||||
|
courseType: '必修课',
|
||||||
|
progress: 40,
|
||||||
|
completedLessons: 6,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 12.3,
|
||||||
|
lastStudyTime: '2024-08-30 11:10',
|
||||||
|
homeworkProgress: 40,
|
||||||
|
examProgress: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
courseName: '计算机网络',
|
||||||
|
courseType: '必修课',
|
||||||
|
progress: 27,
|
||||||
|
completedLessons: 4,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 8.7,
|
||||||
|
lastStudyTime: '2024-08-28 15:30',
|
||||||
|
homeworkProgress: 20,
|
||||||
|
examProgress: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
courseName: '操作系统原理',
|
||||||
|
courseType: '必修课',
|
||||||
|
progress: 13,
|
||||||
|
completedLessons: 2,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 4.2,
|
||||||
|
lastStudyTime: '2024-08-25 14:20',
|
||||||
|
homeworkProgress: 10,
|
||||||
|
examProgress: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
courseName: '人工智能基础',
|
||||||
|
courseType: '选修课',
|
||||||
|
progress: 0,
|
||||||
|
completedLessons: 0,
|
||||||
|
totalLessons: 15,
|
||||||
|
studyTime: 0,
|
||||||
|
lastStudyTime: '未开始',
|
||||||
|
homeworkProgress: 0,
|
||||||
|
examProgress: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 图表配置
|
||||||
|
const chartOption = computed(() => {
|
||||||
|
// 模拟时间序列数据
|
||||||
|
const dateRange = [
|
||||||
|
'2022.04.01', '2022.04.02', '2022.04.03', '2022.04.04', '2022.04.05',
|
||||||
|
'2022.04.06', '2022.04.07', '2022.04.08', '2022.04.09', '2022.04.10', '2022.04.11'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 模拟学习进度数据(按照图片中的趋势)
|
||||||
|
const progressData = [
|
||||||
|
2000, 3500, 5200, 7800, 9500, 11200, 12000, 11800, 10500, 11600, 8000
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: {
|
||||||
|
text: `${studentInfo.value.name}的学习进度明细`,
|
||||||
|
left: 20,
|
||||||
|
top: 20,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'line',
|
||||||
|
lineStyle: {
|
||||||
|
color: '#999',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#333'
|
||||||
|
},
|
||||||
|
formatter: function (params: any) {
|
||||||
|
const param = params[0]
|
||||||
|
return `${param.axisValue}<br/>
|
||||||
|
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${param.color};"></span>
|
||||||
|
学习进度: ${param.value} 分钟`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '5%',
|
||||||
|
right: '5%',
|
||||||
|
bottom: '8%',
|
||||||
|
top: '15%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: dateRange,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
interval: 0,
|
||||||
|
rotate: 0
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e8e8e8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 14000,
|
||||||
|
interval: 2000,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}',
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#f0f0f0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '学习进度',
|
||||||
|
type: 'line',
|
||||||
|
data: progressData,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#1890ff',
|
||||||
|
width: 3
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1890ff',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(24, 144, 255, 0.3)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(24, 144, 255, 0.05)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1890ff',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 3,
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(24, 144, 255, 0.3)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.student-progress {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-course-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-items {
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-duration {
|
||||||
|
width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-status {
|
||||||
|
width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-start-time {
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-complete-time {
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-progress {
|
||||||
|
width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-thumbnail {
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-icon {
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-title {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-time {
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complete-time {
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper .n-progress {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.student-progress {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
padding: 15px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-items {
|
||||||
|
display: none;
|
||||||
|
/* 在移动端隐藏表头 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-course-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stats>* {
|
||||||
|
width: 100% !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|