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>
|
||||
管理班级
|
||||
</n-button>
|
||||
<n-button type="primary" ghost @click="showInviteModal = true">
|
||||
<n-button type="primary" ghost @click="openInviteModal(selectedDepartment || props.classId?.toString() || '1')">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<QrCode />
|
||||
@ -65,7 +65,7 @@
|
||||
<n-button type="primary" @click="openAddModal" v-else-if="props.type === 'student'">
|
||||
添加学员
|
||||
</n-button>
|
||||
<n-button type="primary" ghost v-if="props.type === 'student'">
|
||||
<n-button type="primary" ghost v-if="props.type === 'student'" @click="handleStatisticsAnalysis">
|
||||
统计分析
|
||||
</n-button>
|
||||
<n-button type="primary" ghost @click="showImportModal = true">
|
||||
@ -145,50 +145,6 @@
|
||||
</n-card>
|
||||
</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-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">
|
||||
<div class="invite-content">
|
||||
<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>
|
||||
<n-button ghost type="primary" @click="copyInviteCode">复制</n-button>
|
||||
</div>
|
||||
@ -354,7 +313,8 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
NDataTable,
|
||||
@ -371,6 +331,7 @@ import {
|
||||
NCheckbox,
|
||||
NDropdown,
|
||||
useMessage,
|
||||
useDialog,
|
||||
type FormInst,
|
||||
type FormRules
|
||||
} from 'naive-ui'
|
||||
@ -380,11 +341,13 @@ import ImportModal from '@/components/common/ImportModal.vue'
|
||||
// 定义 props 类型
|
||||
interface Props {
|
||||
type: 'course' | 'student'
|
||||
classId?: number | null // 新增班级ID参数
|
||||
}
|
||||
|
||||
// 接收 props
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'student'
|
||||
type: 'student',
|
||||
classId: null
|
||||
})
|
||||
|
||||
// 定义数据类型
|
||||
@ -418,8 +381,11 @@ interface FormData {
|
||||
|
||||
const totalStudents = ref(1333)
|
||||
const inviteCode = ref('56685222')
|
||||
const currentInviteClassId = ref<string | null>(null) // 当前邀请码对应的班级ID
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const selectedDepartment = ref('')
|
||||
@ -429,7 +395,6 @@ const showInviteModal = ref(false)
|
||||
const showTransferModal = ref(false)
|
||||
const showAddClassModal = ref(false)
|
||||
const showManageClassModal = ref(false)
|
||||
const showDeleteConfirmModal = ref(false)
|
||||
const showImportModal = ref(false)
|
||||
const selectedTargetClass = ref('')
|
||||
const currentTransferStudent = ref<StudentItem | null>(null)
|
||||
@ -438,8 +403,6 @@ const classFormRef = ref<FormInst | null>(null)
|
||||
const isEditMode = ref(false)
|
||||
const currentEditId = ref('')
|
||||
const isRenameMode = ref(false)
|
||||
const currentDeleteClass = ref<any>(null)
|
||||
const showBatchDeleteModal = ref(false)
|
||||
const showBatchTransferModal = ref(false)
|
||||
const selectedRowKeys = ref<string[]>([]) // 多选行的keys
|
||||
|
||||
@ -466,7 +429,7 @@ const rules: FormRules = {
|
||||
{ required: true, message: '请输入学员学号', trigger: 'blur' }
|
||||
],
|
||||
loginPassword: [
|
||||
{ required: true, message: '请输入登录密码', trigger: 'blur' },
|
||||
// { required: true, message: '请输入登录密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
],
|
||||
college: [
|
||||
@ -728,12 +691,12 @@ const pagination = ref({
|
||||
pageSizes: [10, 20, 50],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page
|
||||
loadData()
|
||||
loadData(props.classId)
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
loadData()
|
||||
loadData(props.classId)
|
||||
}
|
||||
})
|
||||
|
||||
@ -742,6 +705,11 @@ const handleTransfer = (row: StudentItem) => {
|
||||
currentTransferStudent.value = row
|
||||
selectedTargetClass.value = ''
|
||||
showTransferModal.value = true
|
||||
|
||||
console.log('打开调班弹窗:', {
|
||||
学员信息: row,
|
||||
可选班级: classOptions.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (row: StudentItem) => {
|
||||
@ -764,7 +732,37 @@ const handleBatchDelete = () => {
|
||||
message.warning('请先选择要移除的学员')
|
||||
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
|
||||
}
|
||||
|
||||
// 确认批量删除
|
||||
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 () => {
|
||||
if (!selectedTargetClass.value) {
|
||||
@ -830,7 +802,7 @@ const confirmBatchTransfer = async () => {
|
||||
selectedRowKeys.value = []
|
||||
|
||||
// 重新加载数据
|
||||
loadData()
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
message.error('批量调班失败,请重试')
|
||||
}
|
||||
@ -842,16 +814,56 @@ const handleDelete = (row: StudentItem) => {
|
||||
|
||||
// 查看学习进度处理函数(student 模式)
|
||||
const handleViewProgress = (row: StudentItem) => {
|
||||
message.info(`查看 ${row.studentName} 的学习进度`)
|
||||
// TODO: 实现查看学习进度的具体逻辑
|
||||
console.log('查看学习进度:', row)
|
||||
// 跳转到学习进度页面,传递学员信息作为查询参数
|
||||
router.push({
|
||||
name: 'StudentProgress',
|
||||
query: {
|
||||
studentId: row.id,
|
||||
studentName: row.studentName,
|
||||
accountNumber: row.accountNumber,
|
||||
className: row.className
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 统计分析处理函数(student 模式)
|
||||
const handleStatisticsAnalysis = () => {
|
||||
// 跳转到统计分析页面
|
||||
router.push({
|
||||
name: 'StatisticsAnalysis'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除学员处理函数(student 模式)
|
||||
const handleDeleteStudent = (row: StudentItem) => {
|
||||
message.warning(`删除学员:${row.studentName}`)
|
||||
// TODO: 实现删除学员的具体逻辑
|
||||
console.log('删除学员:', row)
|
||||
dialog.info({
|
||||
title: '确认删除',
|
||||
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 () => {
|
||||
@ -884,7 +896,7 @@ const handleConfirmTransfer = async () => {
|
||||
selectedTargetClass.value = ''
|
||||
|
||||
// 重新加载数据
|
||||
loadData()
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
message.error('调班失败,请重试')
|
||||
}
|
||||
@ -892,14 +904,46 @@ const handleConfirmTransfer = async () => {
|
||||
|
||||
// 判断是否为当前班级
|
||||
const isCurrentClass = (classValue: string) => {
|
||||
if (!currentTransferStudent.value) return false
|
||||
if (!currentTransferStudent.value) {
|
||||
console.log('调班判断: 未选中学员')
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据学员的className匹配班级value
|
||||
const studentClassName = currentTransferStudent.value.className
|
||||
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
|
||||
}
|
||||
|
||||
// 根据班级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 = () => {
|
||||
navigator.clipboard.writeText(inviteCode.value).then(() => {
|
||||
message.success('邀请码已复制到剪贴板')
|
||||
@ -925,7 +969,7 @@ const handleSubmit = async () => {
|
||||
resetForm()
|
||||
|
||||
// 重新加载数据
|
||||
loadData()
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
message.error('请检查表单信息')
|
||||
}
|
||||
@ -1040,52 +1084,120 @@ const handleRenameClass = (classItem: any) => {
|
||||
|
||||
// 删除班级确认
|
||||
const handleDeleteClass = (classItem: any) => {
|
||||
currentDeleteClass.value = classItem
|
||||
showDeleteConfirmModal.value = true
|
||||
}
|
||||
|
||||
// 确认删除班级
|
||||
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 === currentDeleteClass.value.id)
|
||||
if (index > -1) {
|
||||
masterClassList.value.splice(index, 1)
|
||||
dialog.info({
|
||||
title: '确认删除',
|
||||
content: `确定要删除班级"${classItem.className}"吗?\n\n删除后将无法恢复!`,
|
||||
positiveText: '确认删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
// 这里模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
message.success(`已删除班级:${classItem.className}`)
|
||||
|
||||
// 从主数据源中移除
|
||||
const index = masterClassList.value.findIndex(item => item.id === classItem.id)
|
||||
if (index > -1) {
|
||||
masterClassList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
message.error('删除失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭确认弹窗
|
||||
showDeleteConfirmModal.value = false
|
||||
currentDeleteClass.value = null
|
||||
|
||||
} catch (error) {
|
||||
message.error('删除失败,请重试')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 模拟数据加载
|
||||
const loadData = async () => {
|
||||
const loadData = async (classId?: number | null) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const mockData: StudentItem[] = Array.from({ length: 3 }, (_, index) => ({
|
||||
id: `student_${index + 1}`,
|
||||
studentName: ['张华', '李明', '王丽'][index],
|
||||
accountNumber: [`${(1660340 + index + 1).toString()}`, `${(1660340 + index + 2).toString()}`, `${(1660340 + index + 3).toString()}`][index],
|
||||
className: '计算机1',
|
||||
college: '清华大学经管学院',
|
||||
loginName: [`${(1660340 + index + 1).toString()}`, `${(1660340 + index + 2).toString()}`, `${(1660340 + index + 3).toString()}`][index],
|
||||
joinTime: '2025.07.25 08:20'
|
||||
}))
|
||||
// 根据班级ID模拟不同的数据
|
||||
let mockData: StudentItem[] = []
|
||||
|
||||
if (classId === null || classId === undefined) {
|
||||
// 未选择班级时显示空数据或默认数据
|
||||
mockData = []
|
||||
} else {
|
||||
// 根据不同班级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
|
||||
|
||||
// 更新学员总数
|
||||
totalStudents.value = mockData.length
|
||||
|
||||
console.log(`加载班级 ${classId} 的数据,共 ${mockData.length} 名学员`)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
message.error('加载数据失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -1097,7 +1209,7 @@ const handleImportSuccess = (result: any) => {
|
||||
message.success(`导入完成!成功:${result.details?.success || 0} 条,失败:${result.details?.failed || 0} 条`)
|
||||
|
||||
// 重新加载数据
|
||||
loadData()
|
||||
loadData(props.classId)
|
||||
}
|
||||
|
||||
// 模板下载处理
|
||||
@ -1107,8 +1219,46 @@ const handleTemplateDownload = (type?: string) => {
|
||||
// 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(() => {
|
||||
loadData()
|
||||
// 初始加载时,优先使用使用传入的classId,其次使用选择器的值
|
||||
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
|
||||
loadData(initialClassId)
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
openAddClassModal,
|
||||
handleRenameClass,
|
||||
handleDeleteClass,
|
||||
openInviteModal
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1118,7 +1268,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
/* margin: 16px 0; */
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@ -1341,7 +1491,6 @@ onMounted(() => {
|
||||
|
||||
.batch-delete-content {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.batch-delete-content p {
|
||||
@ -1443,9 +1592,15 @@ onMounted(() => {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.delete-warning {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.delete-confirm-content {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.delete-confirm-content p {
|
||||
@ -1453,16 +1608,6 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.delete-warning {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 批量调班样式 */
|
||||
.batch-transfer-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.batch-transfer-content p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
|
@ -66,6 +66,8 @@ import HomeworkTemplateImport from '@/views/teacher/course/HomeworkTemplateImpor
|
||||
// 学员管理组件
|
||||
import StudentLibrary from '@/views/teacher/student/StudentLibrary.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 ExamQuestionBankManagement from '@/views/teacher/ExamPages/QuestionBankManagement.vue'
|
||||
import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue'
|
||||
@ -310,6 +312,18 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'ClassManagement',
|
||||
component: ClassManagement,
|
||||
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>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
1751
src/views/teacher/student/StatisticsAnalysis.vue
Normal file
@ -33,7 +33,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="student-library" v-else>
|
||||
<ClassManagement type="student"></ClassManagement>
|
||||
<!-- TODO: 暂时传id为1,来显示模拟数据,对接接口需要去掉 -->
|
||||
<ClassManagement type="student" :class-id="1"></ClassManagement>
|
||||
</div>
|
||||
</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>
|