feat:教学楼信息接口联调

master
zhoulexin 3 weeks ago
parent 938d32b926
commit e0f7017bcb

@ -2,90 +2,95 @@ import request from '@/utils/request'
// ==================== 教学楼信息 ====================
// 获取教学楼列表
// 获取教学楼列表(分页)
export function getBuildings(params) {
return request({ url: '/info/buildings', method: 'get', params })
return request({ url: '/building/page', method: 'get', params })
}
// 新增教学楼
export function addBuilding(data) {
return request({ url: '/info/buildings', method: 'post', data })
return request({ url: '/building', method: 'post', data })
}
// 编辑教学楼
export function updateBuilding(data) {
return request({ url: '/info/buildings', method: 'put', data })
return request({ url: `/building/${data.id}`, method: 'put', data })
}
// 删除教学楼
export function deleteBuilding(id) {
return request({ url: `/info/buildings/${id}`, method: 'delete' })
// 删除教学楼(支持批量,传入 id 数组)
export function deleteBuilding(ids) {
return request({ url: '/building', method: 'delete', data: ids })
}
// 获取教室列表(按教学楼id
// 获取教室列表(分页,按教学楼id
export function getRooms(params) {
return request({ url: '/info/rooms', method: 'get', params })
return request({ url: '/classroom/page', method: 'get', params })
}
// 新增教室
export function addRoom(data) {
return request({ url: '/info/rooms', method: 'post', data })
return request({ url: '/classroom', method: 'post', data })
}
// 编辑教室
export function updateRoom(data) {
return request({ url: '/info/rooms', method: 'put', data })
return request({ url: `/classroom/${data.id}`, method: 'put', data })
}
// 删除教室
export function deleteRoom(id) {
return request({ url: `/info/rooms/${id}`, method: 'delete' })
// 删除教室(支持批量,传入 id 数组)
export function deleteRoom(ids) {
return request({ url: '/classroom', method: 'delete', data: ids })
}
// 获取摄像头列表按教室id
export function getCameras(params) {
return request({ url: '/info/cameras', method: 'get', params })
return request({ url: '/device/page', method: 'get', params })
}
// 新增摄像头
export function addCamera(data) {
return request({ url: '/info/cameras', method: 'post', data })
return request({ url: '/device', method: 'post', data })
}
// 编辑摄像头
export function updateCamera(data) {
return request({ url: '/info/cameras', method: 'put', data })
return request({ url: `/device/${data.id}`, method: 'put', data })
}
// 删除摄像头
export function deleteCamera(id) {
return request({ url: `/info/cameras/${id}`, method: 'delete' })
// 删除摄像头(支持批量,传入 id 数组)
export function deleteCamera(ids) {
return request({ url: '/device', method: 'delete', data: ids })
}
// ==================== 班级信息 ====================
// 获取班级列表
// 获取班级列表(分页)
export function getClasses(params) {
return request({ url: '/info/classes', method: 'get', params })
return request({ url: '/class/page', method: 'get', params })
}
// 新增班级
export function addClass(data) {
return request({ url: '/info/classes', method: 'post', data })
return request({ url: '/class', method: 'post', data })
}
// 编辑班级
export function updateClass(data) {
return request({ url: '/info/classes', method: 'put', data })
return request({ url: `/class/${data.id}`, method: 'put', data })
}
// 删除班级
export function deleteClass(id) {
return request({ url: `/info/classes/${id}`, method: 'delete' })
// 删除班级(支持批量,传入 id 数组)
export function deleteClass(ids) {
return request({ url: '/class', method: 'delete', data: ids })
}
// ==================== 教师信息 ====================
// 获取教师列表(下拉用,返回全部)
export function getTeacherList() {
return request({ url: '/teacher/list', method: 'get' })
}
// 获取教师列表(分页 + 关键字搜索)
export function getTeachers(params) {
return request({ url: '/teacher/page', method: 'get', params })

@ -20,7 +20,7 @@
<div class="data-table-card">
<el-table
:data="paginatedBuildings"
:data="buildings"
stripe
@selection-change="handleBuildingSelect"
row-key="id"
@ -45,7 +45,7 @@
</el-table>
<el-pagination
v-model:current-page="buildingPage"
:total="filteredBuildings.length"
:total="buildingTotal"
:page-size="buildingPageSize"
size="small"
background
@ -70,7 +70,7 @@
<div class="data-table-card">
<el-table
:data="paginatedRooms"
:data="filteredRooms"
stripe
@selection-change="handleRoomSelect"
row-key="id"
@ -96,12 +96,13 @@
</el-table>
<el-pagination
v-model:current-page="roomPage"
:total="filteredRooms.length"
:total="roomTotal"
:page-size="roomPageSize"
size="small"
background
layout="total, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
@current-change="loadRooms"
/>
</div>
</div>
@ -120,10 +121,11 @@
</div>
<div class="data-table-card">
<el-table :data="paginatedCameras" stripe @selection-change="handleCameraSelect">
<el-table :data="cameras" stripe @selection-change="handleCameraSelect">
<el-table-column type="selection" width="45" />
<el-table-column prop="deviceNo" label="摄像头编号" min-width="180" />
<el-table-column prop="name" label="摄像头名称" min-width="180" />
<el-table-column prop="ip" label="IP地址" width="160" />
<el-table-column prop="streamUrl" label="流地址" 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">
@ -140,7 +142,7 @@
</el-table>
<el-pagination
v-model:current-page="cameraPage"
:total="filteredCameras.length"
:total="cameraTotal"
:page-size="cameraPageSize"
size="small"
background
@ -200,18 +202,21 @@
<!-- 摄像头弹窗 -->
<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.deviceNo" placeholder="请输入摄像头编号" />
</el-form-item>
<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 label="地址" required>
<el-input v-model="cameraForm.streamUrl" 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-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="安装位置">
@ -233,9 +238,10 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Edit } from '@element-plus/icons-vue'
import { getBuildings, addBuilding, updateBuilding, deleteBuilding, getRooms, addRoom, updateRoom, deleteRoom, getCameras, addCamera, updateCamera, deleteCamera } from '@/api/info'
// ========== ==========
const buildingPage = ref(1)
@ -246,26 +252,47 @@ 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 buildings = ref([])
const buildingTotal = ref(0)
const buildingForm = ref({ name: '', floorCount: 1, status: '启用' })
const loadBuildings = async () => {
try {
const res = await getBuildings({
current: buildingPage.value,
size: buildingPageSize.value,
keyword: buildingSearch.value || undefined
})
const pageData = res.data || {}
buildings.value = (pageData.records || []).map(b => ({
id: b.id,
name: b.buildingName,
floorCount: b.floors,
roomCount: b.classroomCount || 0,
status: b.status === 1 ? '启用' : '停用'
}))
buildingTotal.value = pageData.total || 0
} catch {
//
}
}
const filteredBuildings = computed(() => {
if (!buildingSearch.value) return buildings.value
return buildings.value.filter(b => b.name.includes(buildingSearch.value))
watch(buildingPage, () => loadBuildings())
watch(buildingSearch, () => {
if (buildingPage.value !== 1) {
buildingPage.value = 1
} else {
loadBuildings()
}
})
const paginatedBuildings = computed(() => {
const start = (buildingPage.value - 1) * buildingPageSize.value
return filteredBuildings.value.slice(start, start + buildingPageSize.value)
onMounted(() => {
loadBuildings()
})
const buildingForm = ref({ name: '', floorCount: 1, status: '启用' })
const handleBuildingSelect = (rows) => { selectedBuildings.value = rows }
const handleBuildingRowClick = (row) => { currentBuilding.value = row; roomPage.value = 1; currentRoom.value = null }
const handleBuildingRowClick = (row) => { currentBuilding.value = row; roomPage.value = 1; currentRoom.value = null; loadRooms() }
const showBuildingDialog = (row) => {
if (row) {
@ -278,37 +305,55 @@ const showBuildingDialog = (row) => {
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('添加成功')
const saveBuilding = async () => {
const payload = {
buildingName: buildingForm.value.name,
floors: buildingForm.value.floorCount,
status: buildingForm.value.status === '启用' ? 1 : 0
}
try {
if (buildingEditing.value) {
payload.id = buildingForm.value.id
await updateBuilding(payload)
ElMessage.success('编辑成功')
} else {
await addBuilding(payload)
ElMessage.success('添加成功')
}
buildingDialogVisible.value = false
await loadBuildings()
} catch {
//
}
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('删除成功')
.then(async () => {
try {
await deleteBuilding([row.id])
if (currentBuilding.value?.id === row.id) currentBuilding.value = null
ElMessage.success('删除成功')
await loadBuildings()
} catch {
//
}
})
.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('批量删除成功')
.then(async () => {
try {
const ids = selectedBuildings.value.map(b => b.id)
await deleteBuilding(ids)
currentBuilding.value = null
ElMessage.success('批量删除成功')
await loadBuildings()
} catch {
//
}
})
.catch(() => {})
}
@ -322,32 +367,42 @@ 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 rooms = ref([])
const roomTotal = ref(0)
const loadRooms = async () => {
if (!currentBuilding.value) return
try {
const res = await getRooms({
current: roomPage.value,
size: roomPageSize.value,
buildingId: currentBuilding.value.id
})
const pageData = res.data || {}
rooms.value = (pageData.records || []).map(r => ({
id: r.id,
buildingId: r.buildingId,
roomNo: r.roomNo,
roomName: r.roomName,
capacity: r.capacity,
cameraCount: r.deviceCount || 0,
status: r.status === 1 ? '启用' : '停用'
}))
roomTotal.value = pageData.total || 0
} catch {
//
}
}
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)
if (!roomSearch.value) return rooms.value
return rooms.value.filter(r => r.roomNo.includes(roomSearch.value) || r.roomName.includes(roomSearch.value))
})
const handleRoomSelect = (rows) => { selectedRooms.value = rows }
const handleRoomRowClick = (row) => { currentRoom.value = row; cameraPage.value = 1 }
const handleRoomRowClick = (row) => { currentRoom.value = row; cameraPage.value = 1; loadCameras() }
const showRoomDialog = (row) => {
if (row) {
@ -360,45 +415,60 @@ const showRoomDialog = (row) => {
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('添加成功')
const saveRoom = async () => {
const payload = {
roomNo: roomForm.value.roomNo,
roomName: roomForm.value.roomName,
buildingId: currentBuilding.value.id,
capacity: roomForm.value.capacity,
status: roomForm.value.status === '启用' ? 1 : 0
}
try {
if (roomEditing.value) {
payload.id = roomForm.value.id
await updateRoom(payload)
ElMessage.success('编辑成功')
} else {
await addRoom(payload)
ElMessage.success('添加成功')
}
roomDialogVisible.value = false
await loadRooms()
await loadBuildings()
} catch {
//
}
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('删除成功')
.then(async () => {
try {
await deleteRoom([row.id])
if (currentRoom.value?.id === row.id) currentRoom.value = null
ElMessage.success('删除成功')
await loadRooms()
await loadBuildings()
} catch {
//
}
})
.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('批量删除成功')
.then(async () => {
try {
const ids = selectedRooms.value.map(r => r.id)
await deleteRoom(ids)
currentRoom.value = null
ElMessage.success('批量删除成功')
await loadRooms()
await loadBuildings()
} catch {
//
}
})
.catch(() => {})
}
@ -411,27 +481,43 @@ 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 cameras = ref([])
const cameraTotal = ref(0)
const loadCameras = async () => {
if (!currentRoom.value) return
try {
const res = await getCameras({
current: cameraPage.value,
size: cameraPageSize.value,
classroomId: currentRoom.value.id,
deviceName: cameraSearch.value || undefined
})
const pageData = res.data || {}
cameras.value = (pageData.records || []).map(c => ({
id: c.id,
deviceNo:c.deviceNo,
name: c.deviceName,
streamUrl: c.streamUrl,
streamType: c.streamType,
position: c.location,
status: c.onlineStatus === 1 ? '在线' : '离线'
}))
cameraTotal.value = pageData.total || 0
} catch {
//
}
}
const paginatedCameras = computed(() => {
const start = (cameraPage.value - 1) * cameraPageSize.value
return filteredCameras.value.slice(start, start + cameraPageSize.value)
const cameraForm = ref({ deviceNo: '', name: '', ip: '', streamType: 'webrtc', position: '', status: '在线' })
watch(cameraPage, () => loadCameras())
watch(cameraSearch, () => {
if (cameraPage.value !== 1) {
cameraPage.value = 1
} else {
loadCameras()
}
})
const handleCameraSelect = (rows) => { selectedCameras.value = rows }
@ -442,46 +528,66 @@ const showCameraDialog = (row) => {
cameraForm.value = { ...row }
} else {
cameraEditing.value = false
cameraForm.value = { name: '', ip: '', streamType: 'webrtc', position: '', status: '在线' }
cameraForm.value = { deviceNo: '', 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('添加成功')
const saveCamera = async () => {
const payload = {
deviceNo: cameraForm.value.deviceNo,
deviceType: 'camera',
deviceName: cameraForm.value.name,
streamType: cameraForm.value.streamType,
streamUrl: cameraForm.value.ip,
location: cameraForm.value.position,
onlineStatus: cameraForm.value.status === '在线' ? 1 : 0,
classroomId: currentRoom.value.id
}
try {
if (cameraEditing.value) {
payload.id = cameraForm.value.id
await updateCamera(payload)
ElMessage.success('编辑成功')
} else {
await addCamera(payload)
ElMessage.success('添加成功')
}
cameraDialogVisible.value = false
await loadCameras()
await loadRooms()
} catch {
//
}
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('删除成功')
.then(async () => {
try {
await deleteCamera([row.id])
ElMessage.success('删除成功')
await loadCameras()
await loadRooms()
} catch {
//
}
})
.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('批量删除成功')
.then(async () => {
try {
const ids = selectedCameras.value.map(c => c.id)
await deleteCamera(ids)
ElMessage.success('批量删除成功')
await loadCameras()
await loadRooms()
} catch {
//
}
})
.catch(() => {})
}

@ -7,26 +7,27 @@
<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-date-picker v-model="gradeFilter" type="year" placeholder="年级筛选" clearable size="default" style="width: 160px" value-format="YYYY" />
<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 :data="classes" 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="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 label="班主任" width="100" align="center">
<template #default="{ row }">
{{ teacherOptions.find(t => t.id === row.headteacherId)?.name || '—' }}
</template>
</el-table-column>
<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 :type="row.status === 0 ? 'success' : 'info'" size="small">
{{ row.status === 0 ? '在读' : '毕业' }}
</el-tag>
</template>
</el-table-column>
@ -41,38 +42,42 @@
<el-pagination
v-model:current-page="pageCurrent"
:total="filteredData.length"
:page-size="pageSize"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
size="small"
background
layout="total, prev, pager, next"
layout="total, sizes, prev, pager, next"
style="justify-content: flex-end; margin-top: 16px"
@current-change="fetchClasses"
@size-change="onSizeChange"
/>
</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-form ref="formRef" :model="form" :rules="rules" label-width="90px" label-position="left">
<el-form-item label="班级名称" prop="className">
<el-input v-model="form.className" placeholder="请输入班级全称" />
</el-form-item>
<el-form-item label="入学年份" required>
<el-form-item label="年级" prop="grade">
<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-form-item label="专业" prop="major">
<el-input v-model="form.major" placeholder="请输入专业名称" />
</el-form-item>
<el-form-item label="班主任">
<el-input v-model="form.headTeacher" placeholder="请输入班主任姓名" />
<el-form-item label="班主任" prop="headteacherId">
<el-select v-model="form.headteacherId" placeholder="请选择班主任" clearable 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-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 :value="0">在读</el-radio>
<el-radio :value="1">毕业</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
@ -83,86 +88,46 @@
</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>
<ClassCourseDialog v-model="courseDialogVisible" :class-data="currentClass" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Edit, View } from '@element-plus/icons-vue'
import { addClass, updateClass, getTeacherList, getClasses, deleteClass } from '@/api/info'
import ClassCourseDialog from './components/ClassCourseDialog.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 formRef = ref(null)
const teacherOptions = ref([])
const majors = ['计算机科学与技术', '软件工程', '人工智能', '数据科学与大数据技术', '网络工程']
const rules = {
className: [{ required: true, message: '请输入班级名称', trigger: 'blur' }],
grade: [{ required: true, message: '请选择入学年份', trigger: 'change' }],
major: [{ required: true, message: '请输入专业名称', trigger: 'blur' }],
headteacherId: [{ required: true, message: '请选择班主任', trigger: 'change' }]
}
const form = ref({
id: '',
className: '',
grade: '',
major: '',
headTeacher: '',
headteacherId: '',
studentCount: 0,
status: '在读'
status: 0
})
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 classes = ref([])
const total = ref(0)
const handleSelection = (rows) => { selectedRows.value = rows }
@ -172,88 +137,126 @@ const showDialog = (row) => {
form.value = { ...row }
} else {
editing.value = false
form.value = { id: '', className: '', grade: '', major: '', headTeacher: '', studentCount: 0, status: '在读' }
form.value = { id: '', className: '', grade: '', major: '', headteacherId: '', studentCount: 0, status: 0 }
}
dialogVisible.value = true
}
const saveClass = () => {
if (!form.value.className || !form.value.grade || !form.value.major) {
ElMessage.warning('请填写必填项')
return
const saveClass = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (editing.value) {
await updateClass({
id: form.value.id,
className: form.value.className,
grade: `${form.value.grade}`,
major: form.value.major,
studentCount: form.value.studentCount,
headteacherId: form.value.headteacherId,
status: form.value.status
})
ElMessage.success('编辑成功')
fetchClasses()
} else {
await addClass({
className: form.value.className,
grade: `${form.value.grade}`,
major: form.value.major,
studentCount: form.value.studentCount,
headteacherId: form.value.headteacherId,
status: form.value.status
})
ElMessage.success('添加成功')
fetchClasses()
}
dialogVisible.value = false
} catch {
ElMessage.warning('请完善表单信息')
}
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)
.then(async () => {
await deleteClass([row.id])
ElMessage.success('删除成功')
fetchClasses()
})
.catch(() => {})
}
const batchDelete = () => {
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 个班级?`, '批量删除', { type: 'warning' })
.then(() => {
.then(async () => {
const ids = selectedRows.value.map(r => r.id)
classes.value = classes.value.filter(c => !ids.includes(c.id))
await deleteClass(ids)
ElMessage.success('批量删除成功')
fetchClasses()
})
.catch(() => {})
}
//
const fetchClasses = async () => {
try {
const res = await getClasses({
current: pageCurrent.value,
size: pageSize.value,
keyword: searchKey.value,
grade: gradeFilter.value?`${gradeFilter.value}`:''
})
if (res.code === 200) {
classes.value = res.data.records || []
total.value = res.data.total || 0
}
} catch {
ElMessage.error('获取班级列表失败')
}
}
const onSizeChange = () => {
pageCurrent.value = 1
fetchClasses()
}
//
const fetchTeachers = async () => {
try {
const res = await getTeacherList()
if (res.code === 200) {
teacherOptions.value = res.data || []
}
} catch {
//
}
}
//
watch(searchKey, () => {
pageCurrent.value = 1
fetchClasses()
})
//
watch(gradeFilter, () => {
pageCurrent.value = 1
fetchClasses()
})
onMounted(() => {
fetchClasses()
fetchTeachers()
})
// ========== ==========
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>

@ -0,0 +1,108 @@
<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>
</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>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { Plus, Delete } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
classData: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue', 'closed'])
const visible = computed({
get: () => props.modelValue,
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) => {
if (val && props.classData) {
currentCourses.value = courseMap[props.classData.id] ? [...courseMap[props.classData.id]] : []
}
})
const addCourseRow = () => {
currentCourses.value.push({
courseName: '新课程',
teacherName: '',
weekday: 1,
startPeriod: 1,
endPeriod: 2,
classroom: ''
})
}
</script>
Loading…
Cancel
Save