fix:调整历史记录查询、学生信息菜单

master
zhoulexin 3 weeks ago
parent e0f7017bcb
commit f23d9d98d3

@ -2,6 +2,11 @@ import request from '@/utils/request'
// ==================== 教学楼信息 ====================
// 获取教学楼列表(下拉用,返回全部)
export function getBuildingList() {
return request({ url: '/building/list', method: 'get' })
}
// 获取教学楼列表(分页)
export function getBuildings(params) {
return request({ url: '/building/page', method: 'get', params })
@ -115,3 +120,47 @@ export function getTeacherDetail(id) {
export function deleteTeacher(ids) {
return request({ url: '/teacher', method: 'delete', data: ids })
}
// ==================== 课程信息 ====================
// 获取课程列表(分页)
export function getCourses(params) {
return request({ url: '/course/page', method: 'get', params })
}
// 新增课程
export function addCourse(data) {
return request({ url: '/course', method: 'post', data })
}
// 编辑课程
export function updateCourse(data) {
return request({ url: `/course/${data.id}`, method: 'put', data })
}
// 删除课程(支持批量,传入 id 数组)
export function deleteCourse(ids) {
return request({ url: '/course', method: 'delete', data: ids })
}
// ==================== 课程安排 ====================
// 获取课程安排列表(分页)
export function getSchedulePage(params) {
return request({ url: '/schedule/page', method: 'get', params })
}
// 新增课程安排
export function addSchedule(data) {
return request({ url: '/schedule', method: 'post', data })
}
// 编辑课程安排
export function updateSchedule(data) {
return request({ url: `/schedule/${data.id}`, method: 'put', data })
}
// 删除课程安排(支持批量,传入 id 数组)
export function deleteSchedule(ids) {
return request({ url: '/schedule', method: 'delete', data: ids })
}

@ -46,10 +46,6 @@
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</template>
<el-menu-item index="/settings/personnel">
<el-icon><User /></el-icon>
<span>人员管理</span>
</el-menu-item>
<el-menu-item index="/settings/device">
<el-icon><Monitor /></el-icon>
<span>设备管理</span>
@ -81,6 +77,14 @@
<el-icon><UserFilled /></el-icon>
<span>教师信息</span>
</el-menu-item>
<el-menu-item index="/info/student">
<el-icon><User /></el-icon>
<span>学生信息</span>
</el-menu-item>
<el-menu-item index="/info/course">
<el-icon><Reading /></el-icon>
<span>课程信息</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>

@ -1,4 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const routes = [
{
@ -39,12 +40,6 @@ const routes = [
component: () => import('@/views/bigscreen/index.vue'),
meta: { title: '数据展示大屏', icon: 'DataAnalysis' }
},
{
path: 'settings/personnel',
name: 'Personnel',
component: () => import('@/views/settings/personnel.vue'),
meta: { title: '人员管理', icon: 'User' }
},
{
path: 'settings/device',
name: 'Device',
@ -63,6 +58,12 @@ const routes = [
component: () => import('@/views/settings/permissions.vue'),
meta: { title: '权限管理', icon: 'Lock' }
},
{
path: 'info/student',
name: 'InfoStudent',
component: () => import('@/views/settings/personnel.vue'),
meta: { title: '学生信息', icon: 'User' }
},
{
path: 'info/building',
name: 'InfoBuilding',
@ -80,6 +81,12 @@ const routes = [
name: 'InfoTeacher',
component: () => import('@/views/info/teacher.vue'),
meta: { title: '教师信息', icon: 'UserFilled' }
},
{
path: 'info/course',
name: 'InfoCourse',
component: () => import('@/views/info/course.vue'),
meta: { title: '课程信息', icon: 'Reading' }
}
]
}
@ -93,13 +100,8 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
document.title = to.meta.title ? `${to.meta.title} - 教室智能人脸考勤系统` : '教室智能人脸考勤系统'
// 登录鉴权:从持久化的 user store 中读取 token
let token = ''
try {
const store = localStorage.getItem('user')
if (store) token = JSON.parse(store).token || ''
} catch { /* ignore */ }
if (!token && to.path !== '/login') {
const store = useUserStore()
if (!store.token && to.path !== '/login') {
next('/login')
} else {
next()

@ -1,5 +1,6 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
@ -10,15 +11,9 @@ const request = axios.create({
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 从 pinia-plugin-persistedstate 持久化的 user store 中读取 token
const store = localStorage.getItem('user')
if (store) {
try {
const { token } = JSON.parse(store)
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
} catch { /* ignore */ }
const store = useUserStore()
if (store.token) {
config.headers.Authorization = `Bearer ${store.token}`
}
return config
},
@ -41,6 +36,8 @@ request.interceptors.response.use(
switch (status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
useUserStore().$patch({ token: '' })
window.location.hash = '#/login'
break
case 403:
ElMessage.error('没有访问权限')

@ -16,15 +16,6 @@
style="width: 200px"
/>
<el-date-picker v-model="query.date" type="date" placeholder="选择日期" size="default" value-format="YYYY-MM-DD" />
<el-select v-model="query.behaviorType" placeholder="行为类型" clearable size="default" style="width: 140px">
<el-option label="全部" value="" />
<el-option
v-for="bt in behaviorTypes"
:key="bt.id"
:label="bt.typeName"
:value="bt.id"
/>
</el-select>
<el-button type="primary" :icon="Search" :loading="loading" @click="doSearch"></el-button>
<el-button :icon="RefreshRight" @click="doReset"></el-button>
</div>
@ -44,14 +35,27 @@
@click="viewDetail(record)"
>
<div class="masonry-thumb">
<div class="thumb-placeholder" :style="{ borderColor: getBorderColor(record.attendanceRate) }">
<div class="attendance-stat">
<span class="attendance-rate">{{ record.attendanceRate }}%</span>
<span class="attendance-label">出勤率</span>
<div class="water-glass">
<div
class="water-fill"
:style="{
height: record.attendanceRate + '%',
background: getWaterGradient(record.attendanceRate)
}"
>
<svg class="water-wave" viewBox="0 0 200 24" preserveAspectRatio="none">
<path d="M0,12 C40,4 60,20 100,12 C140,4 160,20 200,12 L200,24 L0,24 Z" :fill="getBorderColor(record.attendanceRate)" />
</svg>
<svg class="water-wave water-wave-back" viewBox="0 0 200 24" preserveAspectRatio="none">
<path d="M0,12 C40,18 60,6 100,12 C140,18 160,6 200,12 L200,24 L0,24 Z" :fill="getBorderColor(record.attendanceRate)" opacity="0.4" />
</svg>
</div>
<span class="water-value">{{ record.attendanceRate }}%</span>
</div>
<span class="water-label">出勤率</span>
</div>
<div class="masonry-info">
<span class="masonry-course" :title="record.courseName">{{ record.courseName }}</span>
<span class="masonry-time">
<el-icon :size="12"><Clock /></el-icon>
{{ record.attDate }}
@ -61,9 +65,13 @@
<el-divider direction="vertical" />
<span>实到 {{ record.actualCount }}</span>
</div>
<span class="masonry-absent" v-if="record.absentCount > 0">
<span class="masonry-absent">
缺勤 {{ record.absentCount }}
</span>
<div class="masonry-extra">
<span v-if="record.className" class="masonry-tag">{{ record.className }}</span>
<span v-if="record.classroomName" class="masonry-tag">{{ record.classroomName }}</span>
</div>
</div>
</div>
</div>
@ -73,7 +81,7 @@
v-model:current-page="pagination.current"
:total="pagination.total"
:page-size="pagination.size"
small
size="small"
background
layout="prev, pager, next"
@current-change="doSearch"
@ -111,19 +119,16 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { Search, RefreshRight, Clock } from '@element-plus/icons-vue'
import { getAttendanceHistory } from '@/api/history'
import { getBehaviorTypes } from '@/api/behavior'
const loading = ref(false)
const detailVisible = ref(false)
const currentRecord = ref(null)
const behaviorTypes = ref([])
const query = reactive({
courseName: '',
date: null,
behaviorType: ''
date: null
})
const pagination = reactive({
@ -134,16 +139,6 @@ const pagination = reactive({
const records = ref([])
/** 获取行为类型列表 */
const fetchBehaviorTypes = async () => {
try {
const res = await getBehaviorTypes()
if (res && res.data) {
behaviorTypes.value = res.data
}
} catch { /* 错误已在拦截器统一处理 */ }
}
/** 查询 */
const doSearch = async () => {
loading.value = true
@ -152,7 +147,6 @@ const doSearch = async () => {
current: pagination.current,
size: pagination.size,
courseName: query.courseName || undefined,
behaviorTypeId: query.behaviorType || undefined,
attDate: query.date || undefined
}
const res = await getAttendanceHistory(params)
@ -173,18 +167,24 @@ const doSearch = async () => {
const doReset = () => {
query.courseName = ''
query.date = null
query.behaviorType = ''
pagination.current = 1
doSearch()
}
/** 根据出勤率获取边框颜色 */
/** 根据出勤率获取水波颜色 */
const getBorderColor = (rate) => {
if (rate >= 95) return '#52c41a'
if (rate >= 85) return '#faad14'
return '#ff4d4f'
}
/** 根据出勤率获取水面渐变 */
const getWaterGradient = (rate) => {
if (rate >= 95) return 'linear-gradient(180deg, rgba(82,196,26,0.3) 0%, rgba(82,196,26,0.65) 100%)'
if (rate >= 85) return 'linear-gradient(180deg, rgba(250,173,20,0.3) 0%, rgba(250,173,20,0.65) 100%)'
return 'linear-gradient(180deg, rgba(255,77,79,0.3) 0%, rgba(255,77,79,0.65) 100%)'
}
/** 根据出勤率获取 tag 类型 */
const getRateType = (rate) => {
if (rate >= 95) return 'success'
@ -199,7 +199,6 @@ const viewDetail = (record) => {
}
onMounted(() => {
fetchBehaviorTypes()
doSearch()
})
</script>
@ -251,33 +250,74 @@ onMounted(() => {
}
.masonry-thumb {
position: relative;
}
.thumb-placeholder {
height: 140px;
background: #f5f7fa;
background: #f9fafb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-bottom: 3px solid #d9d9d9;
padding: 18px 0 12px;
}
.attendance-stat {
text-align: center;
/* ========== 水球玻璃 ========== */
.water-glass {
width: 110px;
height: 110px;
border-radius: 50%;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.25);
border: 3px solid rgba(255, 255, 255, 0.5);
box-shadow:
0 0 15px rgba(0, 0, 0, 0.08),
inset 0 2px 8px rgba(255, 255, 255, 0.4),
inset 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.attendance-rate {
display: block;
font-size: 36px;
font-weight: 700;
color: #262626;
line-height: 1.2;
.water-fill {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 0%;
transition: height 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
overflow: visible;
}
.water-wave {
position: absolute;
top: -16px;
left: 0;
width: 200%;
height: 20px;
animation: wave 3s linear infinite;
}
.water-wave-back {
animation: wave 4s linear infinite reverse;
}
.attendance-label {
@keyframes wave {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.water-label {
font-size: 12px;
color: #bfbfbf;
color: #8c8c8c;
margin-top: 4px;
}
.water-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
font-weight: 700;
color: #262626;
z-index: 2;
pointer-events: none;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.7);
}
.masonry-info {
@ -287,8 +327,17 @@ onMounted(() => {
gap: 4px;
}
.masonry-course {
font-size: 14px;
font-weight: 600;
color: #262626;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.masonry-time {
font-size: 12px;
font-size: 13px;
color: #525252;
display: flex;
align-items: center;
@ -297,7 +346,7 @@ onMounted(() => {
}
.masonry-stats {
font-size: 12px;
font-size: 13px;
color: #bfbfbf;
display: flex;
align-items: center;
@ -305,10 +354,28 @@ onMounted(() => {
}
.masonry-absent {
font-size: 12px;
font-size: 13px;
color: #ff4d4f;
}
.masonry-extra {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 2px;
}
.masonry-tag {
font-size: 12px;
color: #8c8c8c;
background: #f5f5f5;
border-radius: 3px;
padding: 1px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pagination-wrap {
display: flex;
justify-content: center;

@ -1,56 +1,132 @@
<template>
<el-dialog v-model="visible" :title="`${classData?.className || ''} — 课程安排`" width="800px" align-center destroy-on-close @closed="emit('closed')">
<div class="filter-bar" style="padding: 0 0 16px 0">
<el-button type="primary" :icon="Plus" size="small" @click="addCourseRow"></el-button>
<el-dialog v-model="visible" :title="`${classData?.className || ''} — 课程安排`" width="850px" align-center destroy-on-close @closed="emit('closed')">
<div v-loading="loading" style="min-height: 200px">
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" :icon="Plus" size="small" @click="showAddDialog"></el-button>
<el-button type="danger" :icon="Delete" size="small" :disabled="selectedIds.length === 0" @click="batchDelete"></el-button>
</div>
<span v-if="selectedIds.length > 0" class="toolbar-tip"> {{ selectedIds.length }} </span>
</div>
<el-table ref="tableRef" :data="scheduleList" stripe border size="small" empty-text="" @selection-change="onSelectionChange">
<el-table-column type="selection" width="40" />
<el-table-column label="课程名称" min-width="180">
<template #default="{ row }">{{ getCourseName(row.courseId) }}</template>
</el-table-column>
<el-table-column label="授课教师" width="110">
<template #default="{ row }">{{ getTeacherName(row.teacherId) }}</template>
</el-table-column>
<el-table-column label="星期" width="80" align="center">
<template #default="{ row }">{{ weekLabel[row.weekDay] || row.weekDay }}</template>
</el-table-column>
<el-table-column label="学期" width="120" align="center">
<template #default="{ row }">{{ row.semester }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? '进行中' : '已结束' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="showDetail(row)"></el-button>
<el-button type="primary" link size="small" @click="showEditDialog(row)"></el-button>
<el-button type="danger" link size="small" @click="handleDelete(row.id)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-table :data="currentCourses" stripe border size="small">
<el-table-column label="课程名称" min-width="160">
<template #default="{ row }">
<el-input v-model="row.courseName" placeholder="课程名称" size="small" />
</template>
</el-table-column>
<el-table-column label="授课教师" width="110">
<template #default="{ row }">
<el-input v-model="row.teacherName" placeholder="教师" size="small" />
</template>
</el-table-column>
<el-table-column label="星期" width="90" align="center">
<template #default="{ row }">
<el-select v-model="row.weekday" size="small" style="width: 100%">
<el-option v-for="(w, i) in weekdays" :key="i" :label="w" :value="i + 1" />
</el-select>
</template>
</el-table-column>
<el-table-column label="开始节次" width="90" align="center">
<template #default="{ row }">
<el-input-number v-model="row.startPeriod" :min="1" :max="12" size="small" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="结束节次" width="90" align="center">
<template #default="{ row }">
<el-input-number v-model="row.endPeriod" :min="1" :max="12" size="small" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="教室" width="140">
<template #default="{ row }">
<el-input v-model="row.classroom" placeholder="教室" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" :icon="Delete" @click="currentCourses.splice($index, 1)" />
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
<!-- 新增/编辑课程安排弹窗 -->
<el-dialog v-model="addVisible" :title="isEdit ? '编辑课程安排' : '新增课程安排'" width="500px" append-to-body destroy-on-close @closed="onFormDialogClosed">
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="90px" label-position="left">
<el-form-item label="课程名称" prop="courseId">
<el-select v-model="addForm.courseId" placeholder="请选择课程" filterable style="width: 100%">
<el-option v-for="c in courseOptions" :key="c.id" :label="c.courseName" :value="c.id" />
</el-select>
</el-form-item>
<el-form-item label="授课老师" prop="teacherId">
<el-select v-model="addForm.teacherId" placeholder="请选择老师" filterable style="width: 100%">
<el-option v-for="t in teacherOptions" :key="t.id" :label="t.name" :value="t.id" />
</el-select>
</el-form-item>
<el-form-item label="星期几" prop="weekDay">
<el-select v-model="addForm.weekDay" placeholder="请选择星期" style="width: 100%">
<el-option v-for="w in weekdayOptions" :key="w.value" :label="w.label" :value="w.value" />
</el-select>
</el-form-item>
<el-form-item label="教学楼" prop="buildingId">
<el-select v-model="addForm.buildingId" placeholder="请选择教学楼" @change="onBuildingChange" style="width: 100%">
<el-option v-for="b in buildingOptions" :key="b.id" :label="b.buildingName" :value="b.id" />
</el-select>
</el-form-item>
<el-form-item label="教室" prop="classroomId">
<el-select v-model="addForm.classroomId" placeholder="请选择教室" filterable style="width: 100%">
<el-option v-for="r in filteredRoomOptions" :key="r.id" :label="r.name" :value="r.id" />
</el-select>
</el-form-item>
<el-form-item label="开始节次" prop="startSection">
<el-input-number v-model="addForm.startSection" :min="1" :max="12" style="width: 100%" />
</el-form-item>
<el-form-item label="结束节次" prop="endSection">
<el-input-number v-model="addForm.endSection" :min="1" :max="12" style="width: 100%" />
</el-form-item>
<el-form-item label="开始周" prop="startWeek">
<el-input-number v-model="addForm.startWeek" :min="1" :max="20" style="width: 100%" />
</el-form-item>
<el-form-item label="结束周" prop="endWeek">
<el-input-number v-model="addForm.endWeek" :min="1" :max="20" style="width: 100%" />
</el-form-item>
<el-form-item label="学期" prop="semester">
<el-input v-model="addForm.semester" placeholder="请输入学期2024-2025-1" style="width: 100%" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="addForm.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">结束</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" :loading="addLoading" @click="saveSchedule"></el-button>
</template>
</el-dialog>
<!-- 课程安排详情弹窗 -->
<el-dialog v-model="detailVisible" title="课程安排详情" width="600px" append-to-body destroy-on-close>
<el-descriptions :column="2" border v-if="detailData">
<el-descriptions-item label="课程名称">{{ getCourseName(detailData.courseId) }}</el-descriptions-item>
<el-descriptions-item label="授课教师">{{ getTeacherName(detailData.teacherId) }}</el-descriptions-item>
<el-descriptions-item label="星期">{{ weekLabel[detailData.weekDay] || detailData.weekDay }}</el-descriptions-item>
<el-descriptions-item label="学期">{{ detailData.semester }}</el-descriptions-item>
<el-descriptions-item label="开始节次">{{ detailData.startSection }}</el-descriptions-item>
<el-descriptions-item label="结束节次">{{ detailData.endSection }}</el-descriptions-item>
<el-descriptions-item label="开始周">{{ detailData.startWeek }}</el-descriptions-item>
<el-descriptions-item label="结束周">{{ detailData.endWeek }}</el-descriptions-item>
<el-descriptions-item label="教学楼">{{ getBuildingName(detailData.classroomId) }}</el-descriptions-item>
<el-descriptions-item label="教室">{{ getRoomName(detailData.classroomId) }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="detailData.status === 1 ? 'success' : 'info'" size="small">{{ detailData.status === 1 ? '进行中' : '已结束' }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete } from '@element-plus/icons-vue'
import { getSchedulePage, addSchedule, updateSchedule, deleteSchedule, getTeacherList, getCourses, getRooms, getBuildingList } from '@/api/info'
const props = defineProps({
modelValue: { type: Boolean, default: false },
@ -64,45 +140,280 @@ const visible = computed({
set: (val) => emit('update:modelValue', val)
})
const currentCourses = ref([])
const weekdays = ['一', '二', '三', '四', '五']
//
const courseMap = {
1: [
{ courseName: '数据结构与算法', teacherName: '张明', weekday: 1, startPeriod: 1, endPeriod: 2, classroom: '阶梯教室101' },
{ courseName: '操作系统', teacherName: '李华', weekday: 2, startPeriod: 3, endPeriod: 4, classroom: '多媒体教室102' },
{ courseName: '计算机网络', teacherName: '王芳', weekday: 3, startPeriod: 1, endPeriod: 2, classroom: '多媒体教室201' },
{ courseName: '数据库原理', teacherName: '陈伟', weekday: 4, startPeriod: 5, endPeriod: 6, classroom: '多媒体教室201' }
],
2: [
{ courseName: '计算机网络', teacherName: '王芳', weekday: 1, startPeriod: 3, endPeriod: 4, classroom: '多媒体教室102' },
{ courseName: '数据库原理', teacherName: '陈伟', weekday: 2, startPeriod: 1, endPeriod: 2, classroom: '多媒体教室201' },
{ courseName: '数据结构与算法', teacherName: '张明', weekday: 3, startPeriod: 5, endPeriod: 6, classroom: '阶梯教室101' }
],
3: [
{ courseName: '软件工程', teacherName: '刘洋', weekday: 1, startPeriod: 1, endPeriod: 2, classroom: '多媒体教室101' },
{ courseName: 'Java程序设计', teacherName: '赵丽', weekday: 2, startPeriod: 3, endPeriod: 4, classroom: '多媒体教室102' },
{ courseName: '软件测试', teacherName: '孙磊', weekday: 3, startPeriod: 1, endPeriod: 2, classroom: '多媒体教室201' }
]
}
// classData
watch(() => props.modelValue, (val) => {
const loading = ref(false)
const scheduleList = ref([])
//
const teacherOptions = ref([])
const courseOptions = ref([])
const buildingOptions = ref([])
const allRooms = ref([]) // buildingId
const weekdayOptions = [
{ label: '星期一', value: 1 },
{ label: '星期二', value: 2 },
{ label: '星期三', value: 3 },
{ label: '星期四', value: 4 },
{ label: '星期五', value: 5 },
{ label: '星期六', value: 6 },
{ label: '星期日', value: 7 }
]
const weekLabel = ['', '一', '二', '三', '四', '五', '六', '日']
const getTeacherName = (teacherId) => {
const t = teacherOptions.value.find(item => item.id === teacherId)
return t ? t.name : ''
}
const getCourseName = (courseId) => {
const c = courseOptions.value.find(item => item.id === courseId)
return c ? c.courseName : ''
}
const getRoomName = (roomId) => {
const r = allRooms.value.find(item => item.id === roomId)
return r ? r.name : ''
}
const getBuildingName = (classroomId) => {
const r = allRooms.value.find(item => item.id === classroomId)
if (!r) return ''
const b = buildingOptions.value.find(item => item.id === r.buildingId)
return b ? b.buildingName : ''
}
//
const fetchBaseData = async () => {
try {
const [teacherRes, courseRes, roomRes, buildingRes] = await Promise.all([
getTeacherList(),
getCourses({ current: 1, size: 9999 }),
getRooms({ current: 1, size: 9999 }),
getBuildingList()
])
if (teacherRes.code === 200 && teacherRes.data) {
teacherOptions.value = Array.isArray(teacherRes.data) ? teacherRes.data : []
}
if (courseRes.code === 200 && courseRes.data) {
courseOptions.value = courseRes.data.records || []
}
if (roomRes.code === 200 && roomRes.data) {
const list = roomRes.data.records || []
allRooms.value = list.map(r => ({
id: r.id,
name: r.roomName || r.classroomName || `教室${r.id}`,
buildingId: r.buildingId
}))
}
if (buildingRes.code === 200 && buildingRes.data) {
buildingOptions.value = Array.isArray(buildingRes.data) ? buildingRes.data : []
}
} catch {
//
}
}
//
const filteredRoomOptions = computed(() => {
if (!addForm.value.buildingId) return []
return allRooms.value.filter(r => r.buildingId === addForm.value.buildingId)
})
//
const onBuildingChange = () => {
addForm.value.classroomId = null
}
//
const fetchSchedules = async () => {
if (!props.classData) return
try {
const res = await getSchedulePage({
current: 1,
size: 9999,
classId: props.classData.id
})
if (res.code === 200 && res.data) {
scheduleList.value = res.data.records || []
}
} catch {
scheduleList.value = []
}
}
//
watch(() => props.modelValue, async (val) => {
if (val && props.classData) {
currentCourses.value = courseMap[props.classData.id] ? [...courseMap[props.classData.id]] : []
loading.value = true
scheduleList.value = []
try {
await Promise.all([fetchBaseData(), fetchSchedules()])
} catch {
// ignore
} finally {
loading.value = false
}
}
})
const addCourseRow = () => {
currentCourses.value.push({
courseName: '新课程',
teacherName: '',
weekday: 1,
startPeriod: 1,
endPeriod: 2,
classroom: ''
})
// ==================== / ====================
const addVisible = ref(false)
const addLoading = ref(false)
const addFormRef = ref(null)
const isEdit = ref(false)
const editId = ref(null)
const addRules = {
courseId: [{ required: true, message: '请选择课程', trigger: 'change' }],
teacherId: [{ required: true, message: '请选择授课老师', trigger: 'change' }],
weekDay: [{ required: true, message: '请选择星期', trigger: 'change' }],
buildingId: [{ required: true, message: '请选择教学楼', trigger: 'change' }],
classroomId: [{ required: true, message: '请选择教室', trigger: 'change' }],
startSection: [{ required: true, message: '请输入开始节次', trigger: 'blur' }],
endSection: [{ required: true, message: '请输入结束节次', trigger: 'blur' }],
startWeek: [{ required: true, message: '请输入开始周', trigger: 'blur' }],
endWeek: [{ required: true, message: '请输入结束周', trigger: 'blur' }]
}
const defaultForm = () => ({
courseId: null,
teacherId: null,
weekDay: 1,
buildingId: null,
classroomId: null,
startSection: 1,
endSection: 2,
startWeek: 1,
endWeek: 16,
semester: '',
status: 1
})
const addForm = ref(defaultForm())
const onFormDialogClosed = () => {
isEdit.value = false
editId.value = null
}
const showAddDialog = () => {
addForm.value = defaultForm()
isEdit.value = false
editId.value = null
addVisible.value = true
}
const showEditDialog = (row) => {
// classroomId buildingId
const room = allRooms.value.find(r => r.id === row.classroomId)
addForm.value = {
courseId: row.courseId,
teacherId: row.teacherId,
weekDay: row.weekDay,
buildingId: room ? room.buildingId : null,
classroomId: row.classroomId,
startSection: row.startSection,
endSection: row.endSection,
startWeek: row.startWeek,
endWeek: row.endWeek,
semester: row.semester || '',
status: row.status
}
isEdit.value = true
editId.value = row.id
addVisible.value = true
}
const saveSchedule = async () => {
if (!addFormRef.value) return
try {
await addFormRef.value.validate()
addLoading.value = true
const payload = {
courseId: addForm.value.courseId,
teacherId: addForm.value.teacherId,
weekDay: addForm.value.weekDay,
buildingId: addForm.value.buildingId,
classroomId: addForm.value.classroomId,
classId: props.classData.id,
startSection: addForm.value.startSection,
endSection: addForm.value.endSection,
startWeek: addForm.value.startWeek,
endWeek: addForm.value.endWeek,
semester: addForm.value.semester,
status: addForm.value.status
}
if (isEdit.value) {
payload.id = editId.value
await updateSchedule(payload)
ElMessage.success('修改成功')
} else {
await addSchedule(payload)
ElMessage.success('新增成功')
}
addVisible.value = false
await fetchSchedules()
} catch {
//
} finally {
addLoading.value = false
}
}
// ==================== ====================
const detailVisible = ref(false)
const detailData = ref(null)
const showDetail = (row) => {
detailData.value = row
detailVisible.value = true
}
// ==================== ====================
const tableRef = ref(null)
const selectedIds = ref([])
const onSelectionChange = (selection) => {
selectedIds.value = selection.map(item => item.id)
}
const handleDelete = (id) => {
ElMessageBox.confirm('确定要删除该课程安排吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
.then(async () => {
await deleteSchedule([id])
ElMessage.success('删除成功')
await fetchSchedules()
})
.catch(() => {})
}
const batchDelete = () => {
if (selectedIds.value.length === 0) return
ElMessageBox.confirm(`确定要删除选中的 ${selectedIds.value.length} 条课程安排吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
.then(async () => {
await deleteSchedule(selectedIds.value)
ElMessage.success('删除成功')
selectedIds.value = []
await fetchSchedules()
})
.catch(() => {})
}
</script>
<style scoped>
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 16px 0;
}
.toolbar-left {
display: flex;
gap: 10px;
}
.toolbar-tip {
font-size: 13px;
color: #909399;
}
</style>

@ -0,0 +1,286 @@
<template>
<div class="page-container fade-in-up">
<div class="page-header">
<h2 class="page-title">课程信息</h2>
<p class="page-subtitle">管理学校课程基本信息</p>
</div>
<div class="filter-bar">
<el-input v-model="searchCourseName" placeholder="搜索课程名称" :prefix-icon="Search" clearable size="default" style="width: 220px" />
<el-select v-model="searchCourseType" placeholder="课程类型" clearable size="default" style="width: 140px">
<el-option v-for="t in courseTypes" :key="t" :label="t" :value="t" />
</el-select>
<el-select v-model="searchTeacherName" placeholder="教师名称" clearable filterable size="default" style="width: 180px">
<el-option v-for="t in teacherOptions" :key="t.id" :label="t.name" :value="t.name" />
</el-select>
<el-button type="primary" :icon="Plus" @click="showDialog()"></el-button>
<el-button :icon="Delete" :disabled="selectedRows.length === 0" @click="batchDelete"></el-button>
</div>
<div class="data-table-card">
<el-table :data="courses" stripe @selection-change="handleSelection" row-key="id">
<el-table-column type="selection" width="45" />
<el-table-column prop="courseCode" label="课程编码" width="140" />
<el-table-column prop="courseName" label="课程名称" min-width="160" />
<el-table-column prop="courseType" label="课程类型" width="100" align="center" />
<el-table-column prop="credit" label="学分" width="80" align="center" />
<el-table-column label="授课老师" width="120">
<template #default="{ row }">{{ getTeacherName(row.teacherId) }}</template>
</el-table-column>
<el-table-column prop="description" label="课程描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? '开放中' : '已结束' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" :icon="Edit" @click="showDialog(row)"></el-button>
<el-button link type="danger" size="small" :icon="Delete" @click="deleteRow(row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pageCurrent"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
size="small"
background
layout="total, sizes, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
@current-change="fetchCourses"
@size-change="onSizeChange"
/>
</div>
<!-- 添加/编辑课程弹窗 -->
<el-dialog v-model="dialogVisible" :title="editing ? '编辑课程信息' : '添加课程'" width="560px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" label-position="left">
<el-form-item label="课程编码" prop="courseCode">
<el-input v-model="form.courseCode" placeholder="请输入课程编码" />
</el-form-item>
<el-form-item label="课程名称" prop="courseName">
<el-input v-model="form.courseName" placeholder="请输入课程名称" />
</el-form-item>
<el-form-item label="课程类型" prop="courseType">
<el-select v-model="form.courseType" placeholder="请选择课程类型" style="width: 100%">
<el-option v-for="t in courseTypes" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="学分" prop="credit">
<el-input-number v-model="form.credit" :min="0" :max="20" :step="0.5" placeholder="请输入学分" style="width: 100%" />
</el-form-item>
<el-form-item label="授课老师" prop="teacherId">
<el-select v-model="form.teacherId" placeholder="请选择授课老师" filterable style="width: 100%">
<el-option v-for="t in teacherOptions" :key="t.id" :label="t.name" :value="t.id" />
</el-select>
</el-form-item>
<el-form-item label="课程描述">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入课程描述" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :value="1">开放中</el-radio>
<el-radio :value="0">已结束</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveCourse"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Edit } from '@element-plus/icons-vue'
import { addCourse, getCourses, updateCourse, deleteCourse } from '@/api/info'
import { getTeacherList } from '@/api/info'
const searchCourseName = ref('')
const searchCourseType = ref('')
const searchTeacherName = ref('')
const pageCurrent = ref(1)
const pageSize = ref(10)
const dialogVisible = ref(false)
const editing = ref(false)
const selectedRows = ref([])
const formRef = ref(null)
const teacherOptions = ref([])
const courseTypes = ['必修课', '选修课']
const rules = {
courseCode: [{ required: true, message: '请输入课程编码', trigger: 'blur' }],
courseName: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
courseType: [{ required: true, message: '请选择课程类型', trigger: 'change' }],
credit: [{ required: true, message: '请输入学分', trigger: 'blur' }],
teacherId: [{ required: true, message: '请选择授课老师', trigger: 'change' }]
}
const form = ref({
id: '',
courseCode: '',
courseName: '',
courseType: '必修课',
credit: 0,
teacherId: null,
description: '',
status: 1
})
const courses = ref([])
const total = ref(0)
// teacherId
const getTeacherName = (teacherId) => {
const teacher = teacherOptions.value.find(t => t.id === teacherId)
return teacher ? teacher.name : ''
}
const handleSelection = (rows) => { selectedRows.value = rows }
const showDialog = (row) => {
if (row) {
editing.value = true
form.value = {
id: row.id,
courseCode: row.courseCode,
courseName: row.courseName,
courseType: row.courseType,
credit: row.credit,
teacherId: row.teacherId,
description: row.description,
status: row.status
}
} else {
editing.value = false
form.value = { id: '', courseCode: '', courseName: '', courseType: '必修课', credit: 0, teacherId: null, description: '', status: 1 }
}
dialogVisible.value = true
}
const saveCourse = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (editing.value) {
await updateCourse({
id: form.value.id,
courseCode: form.value.courseCode,
courseName: form.value.courseName,
courseType: form.value.courseType,
credit: form.value.credit,
teacherId: form.value.teacherId,
description: form.value.description,
status: form.value.status
})
ElMessage.success('编辑成功')
} else {
await addCourse({
courseCode: form.value.courseCode,
courseName: form.value.courseName,
courseType: form.value.courseType,
credit: form.value.credit,
teacherId: form.value.teacherId,
description: form.value.description,
status: form.value.status
})
ElMessage.success('添加成功')
}
dialogVisible.value = false
fetchCourses()
} catch {
//
}
}
const deleteRow = (row) => {
ElMessageBox.confirm(`确认删除课程 "${row.courseName}"`, '提示', { type: 'warning' })
.then(async () => {
await deleteCourse([row.id])
ElMessage.success('删除成功')
fetchCourses()
})
.catch(() => {})
}
const batchDelete = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 门课程?`, '批量删除', { type: 'warning' })
.then(async () => {
const ids = selectedRows.value.map(r => r.id)
await deleteCourse(ids)
ElMessage.success('批量删除成功')
fetchCourses()
})
.catch(() => {})
}
//
const fetchTeachers = async () => {
try {
const res = await getTeacherList()
if (res.code === 200) {
teacherOptions.value = res.data || []
}
} catch {
//
}
}
//
const fetchCourses = async () => {
try {
const res = await getCourses({
current: pageCurrent.value,
size: pageSize.value,
courseName: searchCourseName.value || undefined,
teacherName: searchTeacherName.value || undefined,
courseType: searchCourseType.value || undefined
})
if (res.code === 200) {
courses.value = res.data.records || []
total.value = res.data.total || 0
}
} catch {
ElMessage.error('获取课程列表失败')
}
}
const onSizeChange = () => {
pageCurrent.value = 1
fetchCourses()
}
watch(searchCourseName, () => {
pageCurrent.value = 1
fetchCourses()
})
watch(searchCourseType, () => {
pageCurrent.value = 1
fetchCourses()
})
watch(searchTeacherName, () => {
pageCurrent.value = 1
fetchCourses()
})
onMounted(() => {
fetchTeachers()
fetchCourses()
})
</script>
<style lang="scss" scoped>
.data-table-card {
background: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
</style>

@ -1,7 +1,7 @@
<template>
<div class="page-container fade-in-up">
<div class="page-header">
<h2 class="page-title">人员管理</h2>
<h2 class="page-title">学生信息</h2>
<p class="page-subtitle">管理学生信息支持批量导入与编辑</p>
</div>

Loading…
Cancel
Save