fix:优化业务逻辑

master
zhoulexin 4 days ago
parent 5a5f5ff1d0
commit e6ef621e46

@ -9,3 +9,13 @@ export const getRecordPage = (params) => {
export const getDetailPage = (params) => {
return request.get('/attendance/detail/page', { params })
}
/** 获取教室当前考勤数据 */
export const getCurrentAttendance = (id, currentTime) => {
return request.get(`/classroom/${id}/current-attendance`, { params: { currentTime } })
}
/** 更新考勤详情状态 */
export const updateDetailStatus = (id, attStatus) => {
return request.put(`/attendance/detail/${id}`, { attStatus })
}

@ -74,10 +74,6 @@
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</template>
<el-menu-item index="/settings/device">
<el-icon><Monitor /></el-icon>
<span>设备管理</span>
</el-menu-item>
<el-menu-item index="/settings/rules">
<el-icon><Notebook /></el-icon>
<span>考勤规则设置</span>
@ -108,7 +104,12 @@ import { useAppStore } from '@/stores/app'
const route = useRoute()
const appStore = useAppStore()
const activeMenu = computed(() => route.path)
const activeMenu = computed(() => {
const path = route.path
// /bigscreen/room/:id ""
if (path.startsWith('/bigscreen')) return '/bigscreen'
return path
})
</script>
<style lang="scss" scoped>

@ -40,12 +40,6 @@ const routes = [
component: () => import('@/views/bigscreen/index.vue'),
meta: { title: '数据展示大屏', icon: 'DataAnalysis' }
},
{
path: 'settings/device',
name: 'Device',
component: () => import('@/views/settings/device.vue'),
meta: { title: '设备管理', icon: 'Monitor' }
},
{
path: 'settings/rules',
name: 'Rules',

@ -99,7 +99,7 @@
<div v-if="imageRecords.length" class="image-actions">
<el-pagination
small
size="small"
background
layout="prev, pager, next"
:total="pagination.total"

@ -76,7 +76,7 @@
<div class="monitor-header">
<h3>教室数据概览</h3>
<div class="monitor-selects">
<el-select v-model="selectedBuildingId" placeholder="选择教学楼" size="small" class="monitor-select" popper-class="monitor-select-popper" :teleported="false" @change="onBuildingChange">
<el-select v-model="selectedBuildingId" placeholder="选择教学楼" size="small" class="monitor-select" popper-class="monitor-select-popper" :teleported="false">
<el-option v-for="b in buildingList" :key="b.id" :label="b.buildingName" :value="b.id" />
</el-select>
</div>
@ -104,7 +104,7 @@
</div>
<div class="room-info-item">
<span class="info-label">摄像头</span>
<span class="info-value">{{ room.cameraCount ?? 0 }} </span>
<span class="info-value">{{ room.deviceCount ?? 0 }} </span>
</div>
</div>
<div class="room-card-footer">
@ -139,14 +139,10 @@ const selectedBuildingId = ref(null)
const filteredRooms = computed(() => {
if (!selectedBuildingId.value) return []
console.log(roomList.value.filter(r => r.buildingId === selectedBuildingId.value),'++++++')
return roomList.value.filter(r => r.buildingId === selectedBuildingId.value)
const id = Number(selectedBuildingId.value)
return roomList.value.filter(r => Number(r.buildingId) === id)
})
const onBuildingChange = () => {
//
}
const goToRoomDetail = (room) => {
router.push({ name: 'BigScreenRoomDetail', params: { id: room.id } })
}

@ -1,45 +1,555 @@
<template>
<div class="room-detail">
<!-- 头部 -->
<div class="detail-header">
<el-button @click="$router.back()"></el-button>
<h2>{{ roomName }} 监控详情</h2>
<el-button :icon="ArrowLeft" @click="$router.back()"></el-button>
<div class="header-info">
<div class="header-title-row">
<h2>{{ room.roomName }} 监控详情</h2>
<el-tag v-if="courseName" type="warning" effect="plain" class="header-tag">
<!-- <el-icon :size="14"><Reading /></el-icon> -->
课程
{{ courseName }}
</el-tag>
<el-tag v-if="teacherName" type="primary" effect="plain" class="header-tag">
<!-- <el-icon :size="14"><UserFilled /></el-icon> -->
授课老师
{{ teacherName }}
</el-tag>
</div>
<div class="header-meta">
<span>教室编号{{ room.roomNo || '-' }}</span>
<i class="meta-divider">|</i>
<span>容纳 {{ room.capacity || '-' }} </span>
<i class="meta-divider">|</i>
<span>摄像头 {{ cameras.length }} </span>
</div>
</div>
<div class="header-status">
<span class="status-dot online" v-if="onlineCount > 0"></span>
<span class="status-dot offline" v-else></span>
<span>{{ onlineCount > 0 ? `${onlineCount} 路在线` : '全部离线' }}</span>
</div>
</div>
<!-- 主体两栏布局 -->
<div class="detail-body">
<p class="placeholder">监控详情页面开发中...</p>
<!-- 左栏摄像头画面 -->
<div class="camera-panel">
<div class="panel-title">
<el-icon :size="18"><VideoCamera /></el-icon>
<span>实时画面</span>
</div>
<div class="camera-grid" :class="{ 'single': cameras.length === 1 }">
<template v-for="camera in cameras" :key="camera.id">
<div class="camera-cell">
<WebRtcPlayer
v-if="camera.streamUrl"
:src="`${camera.streamUrl}/whep`"
@connection-status="(ok) => updateCameraStatus(camera.id, ok)"
/>
<div v-else class="cell-no-stream">
<el-icon :size="36" color="#d9d9d9"><VideoCameraFilled /></el-icon>
</div>
<div class="camera-label">
<span class="dot" :class="getCameraStatus(camera.id)"></span>
{{ camera.name || camera.deviceNo }}
</div>
</div>
</template>
</div>
<div v-if="cameras.length === 0" class="panel-empty">
<el-icon :size="48" color="#d9d9d9"><VideoCameraFilled /></el-icon>
<p>该教室暂未配置摄像头</p>
</div>
</div>
<!-- 右栏人脸识别记录 -->
<div class="recognition-panel">
<div class="panel-title">
<el-icon :size="18"><UserFilled /></el-icon>
<span>人脸识别记录</span>
</div>
<!-- 统计卡片 -->
<div class="recognition-stats">
<div class="stat-card">
<div class="stat-num">{{ stats.total }}</div>
<div class="stat-label">应到</div>
</div>
<div class="stat-card">
<div class="stat-num green">{{ stats.present }}</div>
<div class="stat-label">已到</div>
</div>
<div class="stat-card">
<div class="stat-num orange">{{ stats.missed }}</div>
<div class="stat-label">未到</div>
</div>
<div class="stat-card">
<div class="stat-num blue">{{ stats.rate }}%</div>
<div class="stat-label">出勤率</div>
</div>
</div>
<!-- 学生签到表标题 -->
<div class="sub-title">
<el-icon :size="14"><List /></el-icon>
<span>学生签到表</span>
<span class="sub-title-count" v-if="records.length">{{ records.length }} </span>
</div>
<!-- 识别列表 -->
<div class="recognition-list">
<div
v-for="record in records"
:key="record.id"
class="recognition-item"
>
<div class="rec-avatar">{{ record.studentName ? record.studentName[0] : '?' }}</div>
<div class="rec-info">
<div class="rec-name">{{ record.studentName || '未知' }}</div>
<div class="rec-meta">学号{{ record.studentNo }}</div>
<div class="rec-meta">签到时间{{ formatTime(record.checkInTime) }}</div>
</div>
<el-tag
:type="record.status === '正常' ? 'success' : 'danger'"
size="small"
effect="plain"
>
{{ record.status }}
</el-tag>
<span class="rec-confidence" v-if="record.confidence">{{ record.confidence }}%</span>
</div>
<div v-if="records.length === 0" class="list-empty"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { ArrowLeft, Reading, List } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getCameras, getRoomsList } from '@/api/info'
import { getCurrentAttendance } from '@/api/attendance'
import WebRtcPlayer from '@/components/WebRtcPlayer.vue'
const route = useRoute()
const roomName = ref(`教室 #${route.params.id}`)
const roomId = Number(route.params.id)
const room = ref({})
const cameras = ref([])
const records = ref([])
// &
const courseName = ref('')
const teacherName = ref('')
//
const cameraStatus = reactive({})
const getCameraStatus = (id) => cameraStatus[id] || 'connecting'
const updateCameraStatus = (id, ok) => { cameraStatus[id] = ok ? 'online' : 'offline' }
const onlineCount = computed(() => cameras.value.filter(c => getCameraStatus(c.id) === 'online').length)
//
const stats = reactive({ total: 0, present: 0, missed: 0, rate: 0 })
const formatTime = (t) => {
if (!t) return '-'
const d = new Date(t)
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
const now = () => {
const d = new Date()
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
const loadRoomInfo = async () => {
try {
const res = await getRoomsList()
const list = res?.data || []
room.value = list.find(r => r.id === roomId) || { roomName: `教室 #${roomId}` }
} catch { /* 错误已由拦截器统一处理 */ }
}
const loadCameras = async () => {
try {
const res = await getCameras({ classroomId: roomId, current: 1, size: 50 })
const data = res?.data || {}
cameras.value = (data.records || []).map(c => ({
id: c.id,
name: c.deviceName,
deviceNo: c.deviceNo,
streamUrl: c.streamUrl,
online: c.onlineStatus === 1
}))
} catch { /* 错误已由拦截器统一处理 */ }
}
const loadRecords = async () => {
try {
const res = await getCurrentAttendance(roomId, now())
const data = res?.data
if (!data) return
// &
courseName.value = data.courseName || ''
teacherName.value = data.teacherName || ''
//
stats.total = data.totalCount || 0
stats.present = data.actualCount || 0
stats.missed = stats.total - stats.present
stats.rate = data.totalCount > 0 ? Math.round((data.actualCount / data.totalCount) * 100) : 0
//
const list = data.detailList || []
records.value = list.map(r => ({
id: r.id,
studentName: r.studentName || '未知',
studentNo: r.studentNo || '-',
status: r.attStatusDesc || (r.attStatus === 1 ? '正常' : '异常'),
checkInTime: r.checkInTime,
confidence: r.faceSimilarity
}))
} catch { /* 错误已由拦截器统一处理 */ }
}
let refreshTimer = null
onMounted(async () => {
await loadRoomInfo()
await Promise.all([loadCameras(), loadRecords()])
// 10
refreshTimer = setInterval(() => {
loadCameras()
loadRecords()
}, 10000)
})
onUnmounted(() => {
clearInterval(refreshTimer)
})
</script>
<style lang="scss" scoped>
.room-detail {
padding: 24px;
height: 100%;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
/* ===== Header ===== */
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
margin-bottom: 20px;
flex-shrink: 0;
}
.header-info {
flex: 1;
min-width: 0;
}
.header-title-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
h2 {
font-size: 18px;
font-weight: 600;
color: #262626;
margin: 0;
}
}
.header-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
padding: 0 10px;
line-height: 24px;
}
.header-meta {
font-size: 13px;
color: #999;
display: flex;
align-items: center;
gap: 8px;
.meta-divider {
font-style: normal;
color: #e0e0e0;
}
}
.header-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #666;
flex-shrink: 0;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
&.online {
background: #52c41a;
box-shadow: 0 0 6px rgba(82, 196, 26, 0.6);
}
&.offline {
background: #d9d9d9;
}
}
/* ===== Body ===== */
.detail-body {
flex: 1;
min-height: 0;
display: flex;
gap: 20px;
overflow: hidden;
}
.panel-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #262626;
margin-bottom: 14px;
flex-shrink: 0;
}
/* ===== Left: Camera ===== */
.camera-panel {
flex: 1;
min-width: 0;
background: #fff;
border-radius: 10px;
padding: 18px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
.camera-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
overflow-y: auto;
&.single {
grid-template-columns: 1fr;
}
}
.camera-cell {
border-radius: 8px;
overflow: hidden;
border: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
}
.cell-no-stream {
flex: 1;
min-height: 180px;
background: #fafafa;
display: flex;
align-items: center;
justify-content: center;
}
.camera-label {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
font-size: 13px;
color: #525252;
background: #fafafa;
border-top: 1px solid #f0f0f0;
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
&.online {
background: #52c41a;
box-shadow: 0 0 4px rgba(82, 196, 26, 0.5);
}
&.offline {
background: #d9d9d9;
}
&.connecting {
background: #faad14;
animation: pulse 1.2s ease-in-out infinite;
}
}
}
.placeholder {
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
/* ===== Right: Recognition ===== */
.recognition-panel {
width: 360px;
flex-shrink: 0;
background: #fff;
border-radius: 10px;
padding: 18px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 统计卡片 */
.recognition-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 16px;
flex-shrink: 0;
}
.stat-card {
background: #fafafa;
border-radius: 8px;
padding: 14px 12px;
text-align: center;
padding: 80px 0;
border: 1px solid #f0f0f0;
}
.stat-num {
font-size: 26px;
font-weight: 700;
color: #262626;
line-height: 1.2;
&.green { color: #52c41a; }
&.orange { color: #fa8c16; }
&.blue { color: #409eff; }
}
.stat-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 学生签到表标题 */
.sub-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #262626;
margin-bottom: 10px;
flex-shrink: 0;
}
.sub-title-count {
font-weight: 400;
font-size: 13px;
color: #999;
font-size: 16px;
}
/* 识别列表 */
.recognition-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.recognition-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 8px;
border-bottom: 1px solid #f5f5f5;
transition: background 0.2s;
&:hover {
background: #fafafa;
}
}
.rec-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 600;
flex-shrink: 0;
}
.rec-info {
flex: 1;
min-width: 0;
}
.rec-name {
font-size: 14px;
font-weight: 500;
color: #262626;
}
.rec-meta {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.rec-confidence {
font-size: 12px;
color: #52c41a;
font-weight: 600;
flex-shrink: 0;
}
.panel-empty,
.list-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: #999;
font-size: 14px;
p { margin: 0; }
}
</style>

@ -3,7 +3,7 @@
:model-value="props.modelValue"
@update:model-value="emit('update:modelValue', $event)"
:title="`${props.detailData.courseName} - 考勤详情`"
width="1010px"
width="1030px"
destroy-on-close
>
<div class="detail-header">
@ -41,11 +41,35 @@
<span>{{ (row.faceSimilarity * 100).toFixed(1) }}%</span>
</template>
</el-table-column>
<el-table-column prop="attStatus" label="考勤状态" width="100" align="center">
<el-table-column prop="attStatus" label="考勤状态" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" :type="statusTagType(row.attStatus)">
{{ statusLabel(row.attStatus) }}
</el-tag>
<template v-if="editingId === row.id">
<el-select
ref="selectRef"
v-model="row.attStatus"
size="small"
style="width: 90px"
@change="(val) => handleStatusChange(row, val)"
@visible-change="(visible) => { if (!visible) editingId = null }"
>
<el-option label="未签到" :value="0" />
<el-option label="正常" :value="1" />
<el-option label="迟到" :value="2" />
<el-option label="缺勤" :value="3" />
<el-option label="早退" :value="4" />
<el-option label="请假" :value="5" />
</el-select>
</template>
<template v-else>
<el-tag
size="small"
:type="statusTagType(row.attStatus)"
style="cursor: pointer"
@click="startEditing(row)"
>
{{ statusLabel(row.attStatus) }}
</el-tag>
</template>
</template>
</el-table-column>
</el-table>
@ -70,9 +94,9 @@
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ref, reactive, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { getDetailPage } from '@/api/attendance'
import { getDetailPage, updateDetailStatus } from '@/api/attendance'
const props = defineProps({
modelValue: { type: Boolean, default: false },
@ -108,6 +132,33 @@ const statusMap = {
const statusLabel = (val) => statusMap[val]?.label || '未知'
const statusTagType = (val) => statusMap[val]?.type || 'info'
//
const editingId = ref(null)
const selectRef = ref(null)
const startEditing = async (row) => {
editingId.value = row.id
await nextTick()
selectRef.value?.focus?.()
}
/** 更新单条考勤状态 */
const handleStatusChange = async (row, newStatus) => {
try {
const res = await updateDetailStatus(row.id, newStatus)
if (res?.code === 200) {
ElMessage.success('状态更新成功')
editingId.value = null
} else {
ElMessage.error(res?.msg || '更新失败')
fetchDetailList()
}
} catch {
ElMessage.error('更新失败')
fetchDetailList()
}
}
//
const selectedRows = ref([])
const handleSelectionChange = (rows) => {

@ -27,7 +27,7 @@
:total="total"
layout="prev, pager, next"
background
small
size="small"
style="margin-top: 16px; justify-content: center"
/>
</div>

@ -37,7 +37,6 @@
<!-- 快捷入口 -->
<div class="quick-actions">
<el-button :icon="Download" @click="handleExportAll"></el-button>
<el-button :icon="VideoCamera" @click="$router.push('/bigscreen')"></el-button>
</div>
@ -98,7 +97,7 @@ import AttendanceTrendChart from './components/AttendanceTrendChart.vue'
import ClassRanking from './components/ClassRanking.vue'
import AttendanceManage from './components/AttendanceManage.vue'
import AttendanceDetail from './components/AttendanceDetail.vue'
import { Plus, Download, VideoCamera } from '@element-plus/icons-vue'
import { VideoCamera } from '@element-plus/icons-vue'
/** 将接口记录转为表格行数据 */
const mapRecord = (item) => ({
@ -172,7 +171,6 @@ const showDetail = (row) => {
// ===== =====
const handleExport = (row) => ElMessage.success(`正在导出 ${row.courseName} 考勤明细...`)
const handleExportAll = () => ElMessage.info('正在生成考勤报表...')
</script>
<style lang="scss" scoped>

@ -128,7 +128,7 @@
<template v-if="currentRoom">
<div class="detail-header">
<div class="breadcrumb-path">
<el-link type="primary" :underline="false" @click="backToBuilding">
<el-link type="primary" underline="never" @click="backToBuilding">
<el-icon :size="14"><OfficeBuilding /></el-icon>
{{ currentBuilding?.name }}
</el-link>
@ -176,8 +176,8 @@
<el-table-column type="selection" width="40" />
<el-table-column prop="deviceNo" label="摄像头编号" min-width="160" />
<el-table-column prop="name" label="摄像头名称" min-width="160" />
<el-table-column prop="streamUrl" label="流地址" width="150" />
<el-table-column prop="streamType" label="流类型" width="80" align="center" />
<el-table-column prop="streamUrl" label="视频流地址" width="150" />
<el-table-column prop="streamType" label="视频流类型" width="80" align="center" />
<el-table-column prop="position" label="安装位置" min-width="180" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
@ -262,11 +262,11 @@
<el-form-item label="摄像头名称" required>
<el-input v-model="cameraForm.name" placeholder="请输入摄像头名称" />
</el-form-item>
<el-form-item label="流地址" required>
<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-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" />

@ -34,7 +34,7 @@
<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="info" size="small" :icon="View" :disabled="row.status === 1" @click="showCourses(row)"></el-button>
<el-button link type="danger" size="small" :icon="Delete" @click="deleteRow(row)"></el-button>
</template>
</el-table-column>

@ -1,148 +0,0 @@
<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-select v-model="buildingFilter" placeholder="教学楼" clearable size="default" style="width: 160px">
<el-option label="第一教学楼" value="a" />
<el-option label="第二教学楼" value="b" />
<el-option label="实验楼" value="c" />
</el-select>
<el-input v-model="searchIp" placeholder="搜索IP地址..." :prefix-icon="Search" clearable size="default" style="width: 200px" />
<el-button type="primary" :icon="Plus" @click="showAddDeviceDialog = true">添加设备</el-button>
<el-button :icon="RefreshRight" @click="refreshDevices"></el-button>
</div>
<div class="data-table-card">
<el-table :data="devices" stripe>
<el-table-column prop="name" label="设备名称" min-width="150" />
<el-table-column prop="classroom" label="所在教室" width="140" />
<el-table-column prop="ip" label="IP地址" width="160">
<template #default="{ row }">
<code class="ip-address">{{ row.ip }}</code>
</template>
</el-table-column>
<el-table-column prop="model" label="设备型号" width="140" />
<el-table-column prop="status" label="在线状态" width="100" align="center">
<template #default="{ row }">
<span class="status-dot" :class="row.status === '在线' ? 'online' : 'offline'"></span>
{{ row.status }}
</template>
</el-table-column>
<el-table-column prop="lastActive" label="最近活跃" width="160" />
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" :icon="Setting" @click="configDevice(row)"></el-button>
<el-button link type="danger" size="small" :icon="Delete" @click="removeDevice(row)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 添加设备弹窗 -->
<el-dialog v-model="showAddDeviceDialog" title="添加设备" width="460px" destroy-on-close>
<el-form :model="newDevice" label-width="80px">
<el-form-item label="设备名称" required>
<el-input v-model="newDevice.name" placeholder="如301教室摄像头" />
</el-form-item>
<el-form-item label="IP地址" required>
<el-input v-model="newDevice.ip" placeholder="如192.168.1.100" />
</el-form-item>
<el-form-item label="所在教室" required>
<el-select v-model="newDevice.classroom" placeholder="选择教室" style="width: 100%">
<el-option label="301教室" value="301教室" />
<el-option label="205教室" value="205教室" />
<el-option label="102实验室" value="102实验室" />
</el-select>
</el-form-item>
<el-form-item label="设备型号">
<el-input v-model="newDevice.model" placeholder="设备型号" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddDeviceDialog = false">取消</el-button>
<el-button type="primary" @click="addDevice"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const buildingFilter = ref('')
const searchIp = ref('')
const showAddDeviceDialog = ref(false)
const newDevice = ref({
name: '',
ip: '',
classroom: '',
model: ''
})
const devices = ref([
{ name: '301教室摄像头A', classroom: '301教室', ip: '192.168.1.101', model: 'Hikvision DS-2CD', status: '在线', lastActive: '2024-06-01 10:30' },
{ name: '205教室摄像头A', classroom: '205教室', ip: '192.168.1.102', model: 'Hikvision DS-2CD', status: '在线', lastActive: '2024-06-01 10:28' },
{ name: '102实验室摄像头A', classroom: '102实验室', ip: '192.168.1.103', model: 'Dahua IPC-HFW', status: '在线', lastActive: '2024-06-01 10:25' },
{ name: '408教室摄像头A', classroom: '408教室', ip: '192.168.1.104', model: 'Hikvision DS-2CD', status: '离线', lastActive: '2024-05-31 16:00' },
{ name: '大阶梯教室摄像头A', classroom: '大阶梯教室', ip: '192.168.1.105', model: 'Dahua IPC-HFW', status: '在线', lastActive: '2024-06-01 10:15' }
])
const refreshDevices = () => {
ElMessage.success('设备状态已刷新')
}
const addDevice = () => {
ElMessage.success('设备添加成功')
showAddDeviceDialog.value = false
}
const configDevice = (row) => {
ElMessage.info(`配置设备: ${row.name}`)
}
const removeDevice = (row) => {
ElMessageBox.confirm(`确认删除设备 ${row.name}`, '提示', { type: 'warning' })
.then(() => ElMessage.success('设备已删除'))
.catch(() => {})
}
</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);
}
.ip-address {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 13px;
background: #f5f7fa;
padding: 2px 8px;
border-radius: 4px;
color: #525252;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
&.online {
background: #52c41a;
box-shadow: 0 0 6px rgba(82, 196, 26, 0.5);
}
&.offline {
background: #d9d9d9;
}
}
</style>

@ -76,12 +76,13 @@
<script setup>
import { ref, computed, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { Check } from '@element-plus/icons-vue'
const activeRole = ref('admin')
const roles = ref([
{ id: 'admin', name: '管理员', icon: 'UserFilled', color: 'linear-gradient(135deg, #52c41a, #73d13d)', userCount: 3 },
{ id: 'staff', name: '教务员', icon: 'Avatar', color: 'linear-gradient(135deg, #1890ff, #40a9ff)', userCount: 8 },
// { id: 'staff', name: '', icon: 'Avatar', color: 'linear-gradient(135deg, #1890ff, #40a9ff)', userCount: 8 },
{ id: 'teacher', name: '教师', icon: 'User', color: 'linear-gradient(135deg, #722ed1, #9254de)', userCount: 25 }
])

@ -135,6 +135,7 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus, Check } from '@element-plus/icons-vue'
const rules = ref({
lateMinutes: 10,

Loading…
Cancel
Save