feat:教师信息接口联调

master
zhoulexin 3 weeks ago
parent 7ff83d303c
commit 938d32b926

@ -0,0 +1,112 @@
import request from '@/utils/request'
// ==================== 教学楼信息 ====================
// 获取教学楼列表
export function getBuildings(params) {
return request({ url: '/info/buildings', method: 'get', params })
}
// 新增教学楼
export function addBuilding(data) {
return request({ url: '/info/buildings', method: 'post', data })
}
// 编辑教学楼
export function updateBuilding(data) {
return request({ url: '/info/buildings', method: 'put', data })
}
// 删除教学楼
export function deleteBuilding(id) {
return request({ url: `/info/buildings/${id}`, method: 'delete' })
}
// 获取教室列表按教学楼id
export function getRooms(params) {
return request({ url: '/info/rooms', method: 'get', params })
}
// 新增教室
export function addRoom(data) {
return request({ url: '/info/rooms', method: 'post', data })
}
// 编辑教室
export function updateRoom(data) {
return request({ url: '/info/rooms', method: 'put', data })
}
// 删除教室
export function deleteRoom(id) {
return request({ url: `/info/rooms/${id}`, method: 'delete' })
}
// 获取摄像头列表按教室id
export function getCameras(params) {
return request({ url: '/info/cameras', method: 'get', params })
}
// 新增摄像头
export function addCamera(data) {
return request({ url: '/info/cameras', method: 'post', data })
}
// 编辑摄像头
export function updateCamera(data) {
return request({ url: '/info/cameras', method: 'put', data })
}
// 删除摄像头
export function deleteCamera(id) {
return request({ url: `/info/cameras/${id}`, method: 'delete' })
}
// ==================== 班级信息 ====================
// 获取班级列表
export function getClasses(params) {
return request({ url: '/info/classes', method: 'get', params })
}
// 新增班级
export function addClass(data) {
return request({ url: '/info/classes', method: 'post', data })
}
// 编辑班级
export function updateClass(data) {
return request({ url: '/info/classes', method: 'put', data })
}
// 删除班级
export function deleteClass(id) {
return request({ url: `/info/classes/${id}`, method: 'delete' })
}
// ==================== 教师信息 ====================
// 获取教师列表(分页 + 关键字搜索)
export function getTeachers(params) {
return request({ url: '/teacher/page', method: 'get', params })
}
// 新增教师
export function addTeacher(data) {
return request({ url: '/teacher', method: 'post', data })
}
// 编辑教师
export function updateTeacher(data) {
return request({ url: `/teacher/${data.id}`, method: 'put', data })
}
// 获取教师详情
export function getTeacherDetail(id) {
return request({ url: `/teacher/${id}`, method: 'get' })
}
// 删除教师(支持批量,传入 id 数组)
export function deleteTeacher(ids) {
return request({ url: '/teacher', method: 'delete', data: ids })
}

@ -63,6 +63,25 @@
<span>权限管理</span> <span>权限管理</span>
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-sub-menu index="/info">
<template #title>
<el-icon><List /></el-icon>
<span>信息管理</span>
</template>
<el-menu-item index="/info/building">
<el-icon><OfficeBuilding /></el-icon>
<span>教学楼信息</span>
</el-menu-item>
<el-menu-item index="/info/class">
<el-icon><School /></el-icon>
<span>班级信息</span>
</el-menu-item>
<el-menu-item index="/info/teacher">
<el-icon><UserFilled /></el-icon>
<span>教师信息</span>
</el-menu-item>
</el-sub-menu>
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
@ -87,6 +106,7 @@ const appStore = useAppStore()
const activeMenu = computed(() => { const activeMenu = computed(() => {
const { path } = route const { path } = route
if (path.startsWith('/settings')) return '/settings' if (path.startsWith('/settings')) return '/settings'
if (path.startsWith('/info')) return '/info'
return path return path
}) })
</script> </script>

@ -62,6 +62,24 @@ const routes = [
name: 'Permissions', name: 'Permissions',
component: () => import('@/views/settings/permissions.vue'), component: () => import('@/views/settings/permissions.vue'),
meta: { title: '权限管理', icon: 'Lock' } meta: { title: '权限管理', icon: 'Lock' }
},
{
path: 'info/building',
name: 'InfoBuilding',
component: () => import('@/views/info/building.vue'),
meta: { title: '教学楼信息', icon: 'OfficeBuilding' }
},
{
path: 'info/class',
name: 'InfoClass',
component: () => import('@/views/info/class.vue'),
meta: { title: '班级信息', icon: 'School' }
},
{
path: 'info/teacher',
name: 'InfoTeacher',
component: () => import('@/views/info/teacher.vue'),
meta: { title: '教师信息', icon: 'UserFilled' }
} }
] ]
} }

@ -0,0 +1,514 @@
<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="section-card">
<div class="section-title">
<el-icon :size="18"><OfficeBuilding /></el-icon>
<span>教学楼列表</span>
</div>
<div class="filter-bar">
<el-input v-model="buildingSearch" placeholder="搜索教学楼名称..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
<el-button type="primary" :icon="Plus" @click="showBuildingDialog()"></el-button>
<el-button :icon="Delete" :disabled="selectedBuildings.length === 0" @click="batchDeleteBuilding"></el-button>
</div>
<div class="data-table-card">
<el-table
:data="paginatedBuildings"
stripe
@selection-change="handleBuildingSelect"
row-key="id"
highlight-current-row
@current-change="handleBuildingRowClick"
>
<el-table-column type="selection" width="45" />
<el-table-column prop="name" label="教学楼名称" min-width="220" />
<el-table-column prop="floorCount" label="楼层数" width="100" align="center" />
<el-table-column prop="roomCount" label="教室数量" width="100" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showBuildingDialog(row)">编辑</el-button>
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteBuildingRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="buildingPage"
:total="filteredBuildings.length"
:page-size="buildingPageSize"
size="small"
background
layout="total, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
/>
</div>
</div>
<!-- 教室区域 -->
<div class="section-card" v-if="currentBuilding">
<div class="section-title">
<el-icon :size="18"><HomeFilled /></el-icon>
<span>{{ currentBuilding.name }} 教室列表</span>
</div>
<div class="filter-bar">
<el-input v-model="roomSearch" placeholder="搜索教室编号..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
<el-button type="primary" :icon="Plus" @click="showRoomDialog()"></el-button>
<el-button :icon="Delete" :disabled="selectedRooms.length === 0" @click="batchDeleteRoom"></el-button>
</div>
<div class="data-table-card">
<el-table
:data="paginatedRooms"
stripe
@selection-change="handleRoomSelect"
row-key="id"
highlight-current-row
@current-change="handleRoomRowClick"
>
<el-table-column type="selection" width="45" />
<el-table-column prop="roomNo" label="教室编号" width="140" />
<el-table-column prop="roomName" label="教室名称" min-width="220" />
<el-table-column prop="capacity" label="容纳人数" width="100" align="center" />
<el-table-column prop="cameraCount" label="摄像头数量" width="110" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showRoomDialog(row)">编辑</el-button>
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteRoomRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="roomPage"
:total="filteredRooms.length"
:page-size="roomPageSize"
size="small"
background
layout="total, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
/>
</div>
</div>
<!-- 摄像头区域 -->
<div class="section-card" v-if="currentRoom">
<div class="section-title">
<el-icon :size="18"><Camera /></el-icon>
<span>{{ currentBuilding.name }} / {{ currentRoom.roomName }} 摄像头列表</span>
</div>
<div class="filter-bar">
<el-input v-model="cameraSearch" placeholder="搜索摄像头名称..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
<el-button type="primary" :icon="Plus" @click="showCameraDialog()"></el-button>
<el-button :icon="Delete" :disabled="selectedCameras.length === 0" @click="batchDeleteCamera"></el-button>
</div>
<div class="data-table-card">
<el-table :data="paginatedCameras" stripe @selection-change="handleCameraSelect">
<el-table-column type="selection" width="45" />
<el-table-column prop="name" label="摄像头名称" min-width="180" />
<el-table-column prop="ip" label="IP地址" width="160" />
<el-table-column prop="streamType" label="流类型" width="100" align="center" />
<el-table-column prop="position" label="安装位置" min-width="200" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '在线' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showCameraDialog(row)">编辑</el-button>
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteCameraRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="cameraPage"
:total="filteredCameras.length"
:page-size="cameraPageSize"
size="small"
background
layout="total, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
/>
</div>
</div>
<!-- 教学楼弹窗 -->
<el-dialog v-model="buildingDialogVisible" :title="buildingEditing ? '编辑教学楼' : '添加教学楼'" width="480px" align-center destroy-on-close>
<el-form :model="buildingForm" label-width="100px" label-position="left">
<el-form-item label="教学楼名称" required>
<el-input v-model="buildingForm.name" placeholder="请输入教学楼名称" />
</el-form-item>
<el-form-item label="楼层数" required>
<el-input-number v-model="buildingForm.floorCount" :min="1" :max="20" style="width: 100%" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="buildingForm.status">
<el-radio value="启用">启用</el-radio>
<el-radio value="停用">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="buildingDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveBuilding"></el-button>
</template>
</el-dialog>
<!-- 教室弹窗 -->
<el-dialog v-model="roomDialogVisible" :title="roomEditing ? '编辑教室' : '添加教室'" width="480px" align-center destroy-on-close>
<el-form :model="roomForm" label-width="100px" label-position="left">
<el-form-item label="教室编号" required>
<el-input v-model="roomForm.roomNo" placeholder="请输入教室编号" />
</el-form-item>
<el-form-item label="教室名称" required>
<el-input v-model="roomForm.roomName" placeholder="请输入教室名称" />
</el-form-item>
<el-form-item label="容纳人数" required>
<el-input-number v-model="roomForm.capacity" :min="10" :max="500" style="width: 100%" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="roomForm.status">
<el-radio value="启用">启用</el-radio>
<el-radio value="停用">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="roomDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveRoom"></el-button>
</template>
</el-dialog>
<!-- 摄像头弹窗 -->
<el-dialog v-model="cameraDialogVisible" :title="cameraEditing ? '编辑摄像头' : '添加摄像头'" width="480px" align-center destroy-on-close>
<el-form :model="cameraForm" label-width="100px" label-position="left">
<el-form-item label="摄像头名称" required>
<el-input v-model="cameraForm.name" placeholder="请输入摄像头名称" />
</el-form-item>
<el-form-item label="IP地址" required>
<el-input v-model="cameraForm.ip" placeholder="例如 192.168.1.100" />
</el-form-item>
<el-form-item label="流类型">
<el-select v-model="cameraForm.streamType" placeholder="请选择流类型" style="width: 100%">
<el-option label="WebRTC" value="webrtc" />
<el-option label="RTSP" value="rtsp" />
<el-option label="RTMP" value="rtmp" />
<el-option label="USB" value="usb" />
</el-select>
</el-form-item>
<el-form-item label="安装位置">
<el-input v-model="cameraForm.position" placeholder="请输入安装位置描述" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="cameraForm.status">
<el-radio value="在线">在线</el-radio>
<el-radio value="离线">离线</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cameraDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveCamera"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Edit } from '@element-plus/icons-vue'
// ========== ==========
const buildingPage = ref(1)
const buildingPageSize = ref(8)
const buildingSearch = ref('')
const buildingDialogVisible = ref(false)
const buildingEditing = ref(false)
const selectedBuildings = ref([])
const currentBuilding = ref(null)
const buildings = ref([
{ id: 1, name: '第一教学楼', floorCount: 5, roomCount: 30, status: '启用' },
{ id: 2, name: '第二教学楼', floorCount: 6, roomCount: 36, status: '启用' },
{ id: 3, name: '实验楼', floorCount: 4, roomCount: 20, status: '启用' }
])
const buildingForm = ref({ name: '', floorCount: 1, status: '启用' })
const filteredBuildings = computed(() => {
if (!buildingSearch.value) return buildings.value
return buildings.value.filter(b => b.name.includes(buildingSearch.value))
})
const paginatedBuildings = computed(() => {
const start = (buildingPage.value - 1) * buildingPageSize.value
return filteredBuildings.value.slice(start, start + buildingPageSize.value)
})
const handleBuildingSelect = (rows) => { selectedBuildings.value = rows }
const handleBuildingRowClick = (row) => { currentBuilding.value = row; roomPage.value = 1; currentRoom.value = null }
const showBuildingDialog = (row) => {
if (row) {
buildingEditing.value = true
buildingForm.value = { ...row }
} else {
buildingEditing.value = false
buildingForm.value = { name: '', floorCount: 1, status: '启用' }
}
buildingDialogVisible.value = true
}
const saveBuilding = () => {
if (buildingEditing.value) {
const idx = buildings.value.findIndex(b => b.id === buildingForm.value.id)
if (idx > -1) buildings.value[idx] = { ...buildingForm.value }
ElMessage.success('编辑成功')
} else {
buildingForm.value.id = Date.now()
buildingForm.value.roomCount = 0
buildings.value.push({ ...buildingForm.value })
ElMessage.success('添加成功')
}
buildingDialogVisible.value = false
}
const deleteBuildingRow = (row) => {
ElMessageBox.confirm(`确认删除教学楼 "${row.name}"`, '提示', { type: 'warning' })
.then(() => {
buildings.value = buildings.value.filter(b => b.id !== row.id)
if (currentBuilding.value?.id === row.id) currentBuilding.value = null
ElMessage.success('删除成功')
})
.catch(() => {})
}
const batchDeleteBuilding = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedBuildings.value.length} 个教学楼?`, '批量删除', { type: 'warning' })
.then(() => {
const ids = selectedBuildings.value.map(b => b.id)
buildings.value = buildings.value.filter(b => !ids.includes(b.id))
currentBuilding.value = null
ElMessage.success('批量删除成功')
})
.catch(() => {})
}
// ========== ==========
const roomPage = ref(1)
const roomPageSize = ref(8)
const roomSearch = ref('')
const roomDialogVisible = ref(false)
const roomEditing = ref(false)
const selectedRooms = ref([])
const currentRoom = ref(null)
const rooms = ref([
{ id: 1, buildingId: 1, roomNo: '101', roomName: '阶梯教室101', capacity: 200, cameraCount: 2, status: '启用' },
{ id: 2, buildingId: 1, roomNo: '102', roomName: '多媒体教室102', capacity: 100, cameraCount: 1, status: '启用' },
{ id: 3, buildingId: 1, roomNo: '201', roomName: '多媒体教室201', capacity: 120, cameraCount: 1, status: '启用' },
{ id: 4, buildingId: 1, roomNo: '301', roomName: '报告厅301', capacity: 300, cameraCount: 3, status: '启用' },
{ id: 5, buildingId: 2, roomNo: '101', roomName: '多媒体教室101', capacity: 100, cameraCount: 1, status: '启用' },
{ id: 6, buildingId: 2, roomNo: '102', roomName: '多媒体教室102', capacity: 100, cameraCount: 1, status: '停用' },
{ id: 7, buildingId: 3, roomNo: 'E101', roomName: '物理实验室', capacity: 60, cameraCount: 1, status: '启用' },
{ id: 8, buildingId: 3, roomNo: 'E201', roomName: '化学实验室', capacity: 50, cameraCount: 1, status: '启用' }
])
const roomForm = ref({ roomNo: '', roomName: '', capacity: 100, status: '启用' })
const filteredRooms = computed(() => {
let list = rooms.value.filter(r => r.buildingId === currentBuilding.value?.id)
if (roomSearch.value) list = list.filter(r => r.roomNo.includes(roomSearch.value) || r.roomName.includes(roomSearch.value))
return list
})
const paginatedRooms = computed(() => {
const start = (roomPage.value - 1) * roomPageSize.value
return filteredRooms.value.slice(start, start + roomPageSize.value)
})
const handleRoomSelect = (rows) => { selectedRooms.value = rows }
const handleRoomRowClick = (row) => { currentRoom.value = row; cameraPage.value = 1 }
const showRoomDialog = (row) => {
if (row) {
roomEditing.value = true
roomForm.value = { ...row }
} else {
roomEditing.value = false
roomForm.value = { roomNo: '', roomName: '', capacity: 100, status: '启用' }
}
roomDialogVisible.value = true
}
const saveRoom = () => {
if (roomEditing.value) {
const idx = rooms.value.findIndex(r => r.id === roomForm.value.id)
if (idx > -1) rooms.value[idx] = { ...roomForm.value }
ElMessage.success('编辑成功')
} else {
roomForm.value.id = Date.now()
roomForm.value.buildingId = currentBuilding.value.id
roomForm.value.cameraCount = 0
rooms.value.push({ ...roomForm.value })
//
const b = buildings.value.find(b => b.id === currentBuilding.value.id)
if (b) b.roomCount = (b.roomCount || 0) + 1
ElMessage.success('添加成功')
}
roomDialogVisible.value = false
}
const deleteRoomRow = (row) => {
ElMessageBox.confirm(`确认删除教室 "${row.roomName}"`, '提示', { type: 'warning' })
.then(() => {
rooms.value = rooms.value.filter(r => r.id !== row.id)
const b = buildings.value.find(b => b.id === currentBuilding.value.id)
if (b && b.roomCount > 0) b.roomCount--
if (currentRoom.value?.id === row.id) currentRoom.value = null
ElMessage.success('删除成功')
})
.catch(() => {})
}
const batchDeleteRoom = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedRooms.value.length} 间教室?`, '批量删除', { type: 'warning' })
.then(() => {
const ids = selectedRooms.value.map(r => r.id)
rooms.value = rooms.value.filter(r => !ids.includes(r.id))
const b = buildings.value.find(b => b.id === currentBuilding.value.id)
if (b) b.roomCount = Math.max(0, b.roomCount - ids.length)
currentRoom.value = null
ElMessage.success('批量删除成功')
})
.catch(() => {})
}
// ========== ==========
const cameraPage = ref(1)
const cameraPageSize = ref(8)
const cameraSearch = ref('')
const cameraDialogVisible = ref(false)
const cameraEditing = ref(false)
const selectedCameras = ref([])
const cameras = ref([
{ id: 1, roomId: 1, name: '前置摄像头', ip: '192.168.1.101', streamType: 'webrtc', position: '黑板正上方', status: '在线' },
{ id: 2, roomId: 1, name: '后置摄像头', ip: '192.168.1.102', streamType: 'rtsp', position: '教室后方中央', status: '在线' },
{ id: 3, roomId: 2, name: '前置摄像头', ip: '192.168.1.103', streamType: 'webrtc', position: '黑板正上方', status: '在线' },
{ id: 4, roomId: 3, name: '前置摄像头', ip: '192.168.1.104', streamType: 'rtmp', position: '黑板正上方', status: '在线' },
{ id: 5, roomId: 4, name: '前置摄像头', ip: '192.168.1.105', streamType: 'webrtc', position: '讲台上方', status: '在线' },
{ id: 6, roomId: 4, name: '左侧摄像头', ip: '192.168.1.106', streamType: 'rtsp', position: '左侧墙壁', status: '在线' },
{ id: 7, roomId: 4, name: '右侧摄像头', ip: '192.168.1.107', streamType: 'usb', position: '右侧墙壁', status: '离线' }
])
const cameraForm = ref({ name: '', ip: '', streamType: 'webrtc', position: '', status: '在线' })
const filteredCameras = computed(() => {
let list = cameras.value.filter(c => c.roomId === currentRoom.value?.id)
if (cameraSearch.value) list = list.filter(c => c.name.includes(cameraSearch.value) || c.ip.includes(cameraSearch.value))
return list
})
const paginatedCameras = computed(() => {
const start = (cameraPage.value - 1) * cameraPageSize.value
return filteredCameras.value.slice(start, start + cameraPageSize.value)
})
const handleCameraSelect = (rows) => { selectedCameras.value = rows }
const showCameraDialog = (row) => {
if (row) {
cameraEditing.value = true
cameraForm.value = { ...row }
} else {
cameraEditing.value = false
cameraForm.value = { name: '', ip: '', streamType: 'webrtc', position: '', status: '在线' }
}
cameraDialogVisible.value = true
}
const saveCamera = () => {
if (cameraEditing.value) {
const idx = cameras.value.findIndex(c => c.id === cameraForm.value.id)
if (idx > -1) cameras.value[idx] = { ...cameraForm.value }
ElMessage.success('编辑成功')
} else {
cameraForm.value.id = Date.now()
cameraForm.value.roomId = currentRoom.value.id
cameras.value.push({ ...cameraForm.value })
const r = rooms.value.find(r => r.id === currentRoom.value.id)
if (r) r.cameraCount = (r.cameraCount || 0) + 1
ElMessage.success('添加成功')
}
cameraDialogVisible.value = false
}
const deleteCameraRow = (row) => {
ElMessageBox.confirm(`确认删除摄像头 "${row.name}"`, '提示', { type: 'warning' })
.then(() => {
cameras.value = cameras.value.filter(c => c.id !== row.id)
const r = rooms.value.find(r => r.id === currentRoom.value.id)
if (r && r.cameraCount > 0) r.cameraCount--
ElMessage.success('删除成功')
})
.catch(() => {})
}
const batchDeleteCamera = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedCameras.value.length} 个摄像头?`, '批量删除', { type: 'warning' })
.then(() => {
const ids = selectedCameras.value.map(c => c.id)
cameras.value = cameras.value.filter(c => !ids.includes(c.id))
const r = rooms.value.find(r => r.id === currentRoom.value.id)
if (r) r.cameraCount = Math.max(0, r.cameraCount - ids.length)
ElMessage.success('批量删除成功')
})
.catch(() => {})
}
</script>
<style lang="scss" scoped>
.section-card {
background: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #262626;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.data-table-card {
margin-top: 0;
}
</style>

@ -0,0 +1,266 @@
<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="searchKey" placeholder="搜索班级名称..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
<el-date-picker v-model="gradeFilter" type="year" placeholder="入学年份筛选" clearable size="default" style="width: 160px" value-format="YYYY" />
<el-select v-model="majorFilter" placeholder="专业筛选" clearable size="default" style="width: 160px">
<el-option v-for="m in majors" :key="m" :label="m" :value="m" />
</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="paginatedData" stripe @selection-change="handleSelection" row-key="id">
<el-table-column type="selection" width="45" />
<el-table-column prop="className" label="班级名称" min-width="220" />
<el-table-column prop="grade" label="入学年份" width="110" align="center" />
<el-table-column prop="major" label="专业" width="160" />
<el-table-column prop="studentCount" label="学生人数" width="100" align="center" />
<el-table-column prop="headTeacher" label="班主任" width="100" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '在读' ? 'success' : row.status === '毕业' ? 'info' : 'warning'" size="small">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" 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="info" size="small" :icon="View" @click="showCourses(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"
:total="filteredData.length"
:page-size="pageSize"
size="small"
background
layout="total, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
/>
</div>
<!-- 添加/编辑班级弹窗 -->
<el-dialog v-model="dialogVisible" :title="editing ? '编辑班级信息' : '添加班级'" width="520px" align-center destroy-on-close>
<el-form :model="form" label-width="90px" label-position="left">
<el-form-item label="班级名称" required>
<el-input v-model="form.className" placeholder="请输入班级全称" />
</el-form-item>
<el-form-item label="入学年份" required>
<el-date-picker v-model="form.grade" type="year" placeholder="请选择入学年份" style="width: 100%" value-format="YYYY" />
</el-form-item>
<el-form-item label="专业" required>
<el-input v-model="form.major" placeholder="请输入专业名称" />
</el-form-item>
<el-form-item label="班主任">
<el-input v-model="form.headTeacher" placeholder="请输入班主任姓名" />
</el-form-item>
<el-form-item label="学生人数">
<el-input-number v-model="form.studentCount" :min="0" :max="200" style="width: 100%" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio value="在读">在读</el-radio>
<el-radio value="实习">实习</el-radio>
<el-radio value="毕业">毕业</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveClass"></el-button>
</template>
</el-dialog>
<!-- 课程安排弹窗 -->
<el-dialog v-model="courseDialogVisible" :title="`${currentClass?.className} — 课程安排`" width="800px" align-center destroy-on-close>
<div class="filter-bar" style="padding: 0 0 16px 0">
<el-button type="primary" :icon="Plus" size="small" @click="addCourseRow"></el-button>
</div>
<el-table :data="currentCourses" stripe border size="small">
<el-table-column prop="courseName" label="课程名称" min-width="160" />
<el-table-column prop="teacherName" label="授课教师" width="100" />
<el-table-column prop="weekday" label="星期" width="80" align="center">
<template #default="{ row }">
{{ ['一', '二', '三', '四', '五'][row.weekday - 1] }}
</template>
</el-table-column>
<el-table-column prop="period" label="节次" width="120">
<template #default="{ row }">
{{ row.startPeriod }}{{ row.endPeriod }}
</template>
</el-table-column>
<el-table-column prop="classroom" label="教室" width="120" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ row, $index }">
<el-button link type="danger" size="small" :icon="Delete" @click="currentCourses.splice($index, 1)">移除</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="courseDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Edit, View } from '@element-plus/icons-vue'
const searchKey = ref('')
const gradeFilter = ref('')
const majorFilter = ref('')
const pageCurrent = ref(1)
const pageSize = ref(10)
const dialogVisible = ref(false)
const editing = ref(false)
const selectedRows = ref([])
const majors = ['计算机科学与技术', '软件工程', '人工智能', '数据科学与大数据技术', '网络工程']
const form = ref({
id: '',
className: '',
grade: '',
major: '',
headTeacher: '',
studentCount: 0,
status: '在读'
})
const classes = ref([
{ id: 1, className: '计算机科学与技术2021级1班', grade: '2021', major: '计算机科学与技术', studentCount: 35, headTeacher: '张明', status: '在读' },
{ id: 2, className: '计算机科学与技术2021级2班', grade: '2021', major: '计算机科学与技术', studentCount: 33, headTeacher: '李华', status: '在读' },
{ id: 3, className: '软件工程2022级1班', grade: '2022', major: '软件工程', studentCount: 40, headTeacher: '王芳', status: '在读' },
{ id: 4, className: '软件工程2022级2班', grade: '2022', major: '软件工程', studentCount: 38, headTeacher: '赵丽', status: '在读' },
{ id: 5, className: '人工智能2023级1班', grade: '2023', major: '人工智能', studentCount: 30, headTeacher: '陈伟', status: '在读' },
{ id: 6, className: '人工智能2023级2班', grade: '2023', major: '人工智能', studentCount: 28, headTeacher: '刘洋', status: '实习' },
{ id: 7, className: '计算机科学与技术2020级1班', grade: '2020', major: '计算机科学与技术', studentCount: 36, headTeacher: '张明', status: '毕业' },
{ id: 8, className: '数据科学与大数据技术2024级1班', grade: '2024', major: '数据科学与大数据技术', studentCount: 32, headTeacher: '孙磊', status: '在读' }
])
const filteredData = computed(() => {
let list = classes.value
if (searchKey.value) list = list.filter(c => c.className.includes(searchKey.value))
if (gradeFilter.value) list = list.filter(c => c.grade === gradeFilter.value)
if (majorFilter.value) list = list.filter(c => c.major === majorFilter.value)
return list
})
const paginatedData = computed(() => {
const start = (pageCurrent.value - 1) * pageSize.value
return filteredData.value.slice(start, start + pageSize.value)
})
const handleSelection = (rows) => { selectedRows.value = rows }
const showDialog = (row) => {
if (row) {
editing.value = true
form.value = { ...row }
} else {
editing.value = false
form.value = { id: '', className: '', grade: '', major: '', headTeacher: '', studentCount: 0, status: '在读' }
}
dialogVisible.value = true
}
const saveClass = () => {
if (!form.value.className || !form.value.grade || !form.value.major) {
ElMessage.warning('请填写必填项')
return
}
if (editing.value) {
const idx = classes.value.findIndex(c => c.id === form.value.id)
if (idx > -1) classes.value[idx] = { ...form.value }
ElMessage.success('编辑成功')
} else {
form.value.id = Date.now()
classes.value.push({ ...form.value })
ElMessage.success('添加成功')
}
dialogVisible.value = false
}
const deleteRow = (row) => {
ElMessageBox.confirm(`确认删除班级 "${row.className}"`, '提示', { type: 'warning' })
.then(() => {
classes.value = classes.value.filter(c => c.id !== row.id)
ElMessage.success('删除成功')
})
.catch(() => {})
}
const batchDelete = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 个班级?`, '批量删除', { type: 'warning' })
.then(() => {
const ids = selectedRows.value.map(r => r.id)
classes.value = classes.value.filter(c => !ids.includes(c.id))
ElMessage.success('批量删除成功')
})
.catch(() => {})
}
// ========== ==========
const courseDialogVisible = ref(false)
const currentClass = ref(null)
const currentCourses = ref([])
//
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' }
]
}
const showCourses = (row) => {
currentClass.value = row
currentCourses.value = courseMap[row.id] ? [...courseMap[row.id]] : []
courseDialogVisible.value = true
}
const addCourseRow = () => {
currentCourses.value.push({
courseName: '新课程',
teacherName: '',
weekday: 1,
startPeriod: 1,
endPeriod: 2,
classroom: ''
})
}
</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>

@ -0,0 +1,317 @@
<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="searchKey" placeholder="搜索姓名/工号/院系" :prefix-icon="Search" clearable size="default" style="width: 220px" />
<el-select v-model="titleFilter" placeholder="职称筛选" clearable size="default" style="width: 160px">
<el-option v-for="t in titles" :key="t" :label="t" :value="t" />
</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="teachers" stripe @selection-change="handleSelection" row-key="id">
<el-table-column type="selection" width="45" />
<el-table-column prop="teacherNo" label="工号" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="gender" label="性别" width="70" align="center">
<template #default="{ row }">{{ row.gender === 1 ? '男' : '女' }}</template>
</el-table-column>
<el-table-column prop="department" label="所属院系" min-width="160" />
<el-table-column prop="title" label="职称" width="120" />
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column prop="email" label="邮箱" min-width="180" />
<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="200" 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="info" size="small" :icon="View" @click="showDetail(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="fetchTeachers"
@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="teacherNo">
<el-input v-model="form.teacherNo" placeholder="请输入工号" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.gender">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="所属院系" prop="department">
<el-input v-model="form.department" placeholder="请输入所属院系" />
</el-form-item>
<el-form-item label="职称" prop="title">
<el-select v-model="form.title" placeholder="请选择职称" style="width: 100%">
<el-option v-for="t in titles" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" 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="saveTeacher"></el-button>
</template>
</el-dialog>
<!-- 教师详情弹窗 -->
<el-dialog v-model="detailVisible" title="教师详情" width="600px" destroy-on-close>
<div class="detail-section" v-loading="detailLoading" v-if="detailTeacher">
<div class="detail-avatar">
<el-icon :size="48"><UserFilled /></el-icon>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="工号">{{ detailTeacher.teacherNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detailTeacher.name }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detailTeacher.gender === 1 ? '男' : '女' }}</el-descriptions-item>
<el-descriptions-item label="院系">{{ detailTeacher.department }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ detailTeacher.title }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ detailTeacher.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ detailTeacher.email }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="detailTeacher.status === 1 ? 'success' : 'info'" size="small">{{ detailTeacher.status === 1 ? '在职' : '离职' }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Edit, View } from '@element-plus/icons-vue'
import { addTeacher, getTeachers, updateTeacher, getTeacherDetail, deleteTeacher } from '@/api/info'
const searchKey = ref('')
const titleFilter = ref('')
const pageCurrent = ref(1)
const pageSize = ref(10)
const dialogVisible = ref(false)
const detailVisible = ref(false)
const editing = ref(false)
const selectedRows = ref([])
const detailTeacher = ref(null)
const detailLoading = ref(false)
const formRef = ref(null)
const rules = {
teacherNo: [{ required: true, message: '请输入工号', trigger: 'blur' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
department: [{ required: true, message: '请输入所属院系', trigger: 'blur' }],
title: [{ required: true, message: '请选择职称', trigger: 'change' }],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
}
const titles = ['教授', '副教授', '讲师', '助教', '研究员']
const form = ref({
id: '',
teacherNo: '',
name: '',
gender: 1,
department: '',
title: '讲师',
phone: '',
email: '',
status: 1
})
const teachers = ref([])
const total = ref(0)
const handleSelection = (rows) => { selectedRows.value = rows }
const showDialog = (row) => {
if (row) {
editing.value = true
form.value = { ...row }
} else {
editing.value = false
form.value = { id: '', teacherNo: '', name: '', gender: 1, department: '', title: '讲师', phone: '', email: '', status: 1 }
}
dialogVisible.value = true
}
const saveTeacher = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (editing.value) {
await updateTeacher({
id: form.value.id,
teacherNo: form.value.teacherNo,
name: form.value.name,
gender: form.value.gender,
title: form.value.title,
department: form.value.department,
phone: form.value.phone,
email: form.value.email,
status: form.value.status
})
ElMessage.success('编辑成功')
fetchTeachers()
} else {
await addTeacher({
teacherNo: form.value.teacherNo,
name: form.value.name,
gender: form.value.gender,
title: form.value.title,
department: form.value.department,
phone: form.value.phone,
email: form.value.email,
status: form.value.status
})
ElMessage.success('添加成功')
fetchTeachers()
}
dialogVisible.value = false
} catch {
}
}
const showDetail = async (row) => {
detailVisible.value = true
detailLoading.value = true
try {
const res = await getTeacherDetail(row.id)
if (res.code === 200) {
detailTeacher.value = res.data
}
} catch {
ElMessage.error('获取教师详情失败')
detailVisible.value = false
} finally {
detailLoading.value = false
}
}
const deleteRow = (row) => {
ElMessageBox.confirm(`确认删除教师 "${row.name}"`, '提示', { type: 'warning' })
.then(async () => {
await deleteTeacher([row.id])
ElMessage.success('删除成功')
fetchTeachers()
})
.catch(() => {})
}
const batchDelete = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 名教师?`, '批量删除', { type: 'warning' })
.then(async () => {
const ids = selectedRows.value.map(r => r.id)
await deleteTeacher(ids)
ElMessage.success('批量删除成功')
fetchTeachers()
})
.catch(() => {})
}
//
const fetchTeachers = async () => {
try {
const res = await getTeachers({
current: pageCurrent.value,
size: pageSize.value,
keyword: searchKey.value,
title: titleFilter.value
})
if (res.code === 200) {
teachers.value = res.data.records || []
total.value = res.data.total || 0
}
} catch {
ElMessage.error('获取教师列表失败')
}
}
const onSizeChange = () => {
pageCurrent.value = 1
fetchTeachers()
}
//
watch(searchKey, () => {
pageCurrent.value = 1
fetchTeachers()
})
//
watch(titleFilter, () => {
pageCurrent.value = 1
fetchTeachers()
})
onMounted(() => {
fetchTeachers()
})
</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);
}
.detail-section {
.detail-avatar {
display: flex;
justify-content: center;
margin-bottom: 20px;
color: #52c41a;
}
}
</style>

@ -46,7 +46,7 @@
v-model:current-page="pageCurrent" v-model:current-page="pageCurrent"
:total="students.length" :total="students.length"
:page-size="10" :page-size="10"
small size="small"
background background
layout="total, prev, pager, next" layout="total, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px" style="justify-content: flex-end; margin-top: 16px"

Loading…
Cancel
Save