Compare commits

...

7 Commits

Author SHA1 Message Date
龚皓 1d550b8952 分布和趋势布局调整,卡片布局大小适配 2024-10-22 15:59:56 +08:00
龚皓 13425be2ca 用户列表查看滚动条 2024-10-22 13:56:23 +08:00
龚皓 6159ce3a18 横向滚动条调整 2024-10-22 13:54:02 +08:00
龚皓 7308c2f9bc 响应布局调整,容器高度限定,滚轮查看设置 2024-10-22 13:51:40 +08:00
龚皓 b02b125dad 高度调整等高 2024-10-22 13:50:24 +08:00
龚皓 8eaafbd577 摄像组件响应布局,缩小动态换行 2024-10-22 13:47:44 +08:00
龚皓 c1bf58a99a 2024-10-22 13:46:12 +08:00
8 changed files with 263 additions and 209 deletions

View File

@ -23,4 +23,4 @@ export default {
margin: 0;
padding: 0;
} */
</style>
</style>

View File

@ -487,8 +487,8 @@ watch(monthlyCounts, () => {
background-color: #304555;
color: #fff;
border-radius: 8px;
padding: 0;
margin: 0;
/* padding: 10px; */
/* margin: 10px; */
}
.alert-header {
@ -498,7 +498,9 @@ watch(monthlyCounts, () => {
}
.chart-container {
height: 350px;
/* min-height: 350px; */
min-height: 41vh;
min-width: 40vw;
}
::v-deep .el-tabs__item {

View File

@ -1,22 +1,30 @@
<template>
<div>
<el-card class="camera-card">
<el-row :gutter="20" class="camera-row">
<el-row class="camera-row">
<!-- 左侧块通道列表 -->
<el-col :span="8">
<el-col :sm="24" :md="8">
<el-card class="channel-card" shadow="hover">
<div class="section-title">通道</div>
<div class="scroll-container">
<el-table :data="cameras" height="250">
<el-table-column prop="id" label="ID" width="100"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table :data="cameras" height="220">
<el-table-column prop="id" label="ID" width="100" align="center"></el-table-column>
<el-table-column prop="name" label="名称" align="center"></el-table-column>
<!-- 添加点播按钮的列 -->
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" @click="handlePlay(scope.row)">
点播
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</el-col>
<!-- 中间块摄像头总数量 -->
<el-col :span="8">
<el-col :sm="24" :md="8">
<el-card class="count-card" shadow="hover">
<div class="section-title">摄像数量</div>
<div class="camera-count-chart"></div>
@ -24,19 +32,37 @@
</el-col>
<!-- 右侧块在线/离线/异常状态 -->
<el-col :span="8">
<el-col :sm="24" :md="8">
<el-card class="online-card" shadow="hover">
<div class="section-title">在线情况</div>
<div class="status-summary-chart"></div>
</el-card>
</el-col>
</el-row>
<!-- 视频播放弹窗 -->
<el-dialog v-model="dialogVisible" width="50%" @close="handleStopStream" v-if="selectedCamera">
<p>
正在播放{{ selectedCamera.name }}
</p>
<div v-if="loading" class="loading-container">
<el-spinner size="large"></el-spinner>
</div>
<div v-show="!loading" class="video-container">
<canvas ref="canvasRef" class="video-canvas"></canvas>
</div>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
import { BoxApi } from '@/utils/boxApi.ts';
@ -45,7 +71,11 @@ const cameraCount = ref(0);
const onlineCount = ref(0);
const offlineCount = ref(0);
const exceptionCount = ref(0);
const selectedCamera = ref(null);
const dialogVisible = ref(false);
const loading = ref(true); //
const canvasRef = ref(null);
const playerRef = ref(null);
const apiInstance = new BoxApi();
//
@ -72,7 +102,7 @@ const fetchCameras = async () => {
cameras.value = allCameras;
// 线线
allCameras.forEach(camera => {
allCameras.forEach((camera) => {
if (camera.status === 'online') {
onlineCount.value++;
} else if (camera.status === 'offline') {
@ -92,6 +122,59 @@ const fetchCameras = async () => {
}
};
//
const handlePlay = (camera) => {
selectedCamera.value = camera;
dialogVisible.value = true;
loading.value = true;
//
setTimeout(() => {
loading.value = false;
handleStartStream(); //
}, 2000); // 2
};
//
const handleStartStream = async () => {
const token = localStorage.getItem('alertToken');
try {
const response = await apiInstance.startCameraStream(token, selectedCamera.value.id);
const streamPort = response.port;
//
const url = `ws://192.168.28.33:${streamPort}/`;
if (playerRef.value) {
playerRef.value.destroy(); //
}
if (window.JSMpeg) {
playerRef.value = new window.JSMpeg.Player(url, {
canvas: canvasRef.value,
autoplay: true,
videoBufferSize: 15 * 1024 * 1024,
audioBufferSize: 5 * 1024 * 1024,
});
}
} catch (error) {
console.error('Error starting stream:', error);
}
};
//
const handleStopStream = async () => {
const token = localStorage.getItem('alertToken');
try {
await apiInstance.stopCameraStream(token, selectedCamera.value.id);
if (playerRef.value) {
playerRef.value.destroy();
playerRef.value = null;
}
} catch (error) {
console.error('Error stopping stream:', error);
}
};
//
const initCountChart = (count) => {
const chartDom = document.querySelector('.camera-count-chart');
@ -109,15 +192,12 @@ const initCountChart = (count) => {
formatter: '{c}',
fontSize: 24,
},
data: [
{ value: count, name: '总数' }
],
data: [{ value: count, name: '总数' }],
itemStyle: {
color: 'rgba(80,160,225, 1)'
}
}
]
color: 'rgba(80,160,225, 1)',
},
},
],
};
myChart.setOption(option);
@ -135,24 +215,31 @@ const initStatusSummaryChart = (online, offline, exception) => {
radius: ['40%', '60%'],
label: {
show: true,
formatter: '{b}: {c} ({d}%)'
formatter: '{b}: {c} ({d}%)',
},
data: [
{ value: online, name: '在线' },
{ value: offline, name: '离线' },
{ value: exception, name: '异常' }
{ value: exception, name: '异常' },
],
itemStyle: {
color: 'rgba(5,192,145, 1)'
// color: 'rgba(255,151,75, 1)'
}
}
]
color: 'rgba(5,192,145, 1)',
},
},
],
};
myChart.setOption(option);
};
//
onBeforeUnmount(() => {
if (playerRef.value) {
playerRef.value.destroy();
playerRef.value = null;
}
});
onMounted(() => {
fetchCameras();
});
@ -160,22 +247,22 @@ onMounted(() => {
<style scoped>
.camera-card {
/* background-color: #abcef1; */
/* background-color: linear-gradient(to top, rgba(150, 206, 243, 0.2), rgba(214, 240, 250, 0.3)); */
color: #fff;
/* border-radius: 8px; */
margin: 0;
padding: 0;
/* padding-left: 5%; */
}
.channel-card {
background: linear-gradient(to top, rgba(150, 206, 243, 1), rgba(125, 29, 235, 0.3));
border-radius: 12px;
border-radius: 12px;
}
.count-card {
background: linear-gradient(to bottom, rgba(246, 130, 85, 1), rgba(252, 186, 38, 0.3));
border-radius: 12px;
margin-left: 20px;
margin-right: 20px;
}
.online-card {
@ -183,43 +270,41 @@ onMounted(() => {
border-radius: 12px;
}
.camera-header {
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #3a4b5c;
}
.camera-row {
margin-top: 0px;
margin-bottom: 0px;
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.el-card {
padding: 0;
text-align: center;
background-color: azure
}
.scroll-container {
height: 170px;
/* overflow-y: scroll; */
scrollbar-width: none;
}
height: 27vh;
/* overflow-y: auto; */
/* .scroll-container::-webkit-scrollbar {
display: none;
} */
}
.status-summary-chart,
.camera-count-chart {
/* width: 100%; */
height: 27vh;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.video-container {
width: 100%;
height: 170px;
height: 400px;
background-color: #000;
}
.video-canvas {
width: 100%;
height: 100%;
}
.el-table {
@ -229,14 +314,23 @@ onMounted(() => {
}
.el-table th {
background-color: #f5f7fa; /* 表头背景色 */
color: #333; /* 表头字体颜色 */
font-weight: bold;
text-align: center; /* 居中对齐 */
background-color: #f5f7fa;
color: #333;
font-weight: bold;
text-align: center;
}
::v-deep .el-card__body{
::v-deep .el-card__body {
padding: 15px;
}
.camera-row {
max-height: 37vh;
/* max-height: 250px; */
/* max-width: 1680px; */
overflow-x: auto;
/* gap: 1px; */
/* flex-wrap: nowrap; */
}
</style>

View File

@ -1,75 +1,59 @@
<template>
<div id="layout" class="layout-container">
<el-aside class="nav-sidebar">
<div class="logo">
<!-- 侧边栏 -->
<el-aside class="nav-sidebar" :style="{ width: isCollapse ? '64px' : '180px' }">
<div class="logo" v-if="!isCollapse">
<el-image src="/turing.png" fit="contain" />
</div>
<!-- <el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
<el-radio-button :value="false">expand</el-radio-button>
<el-radio-button :value="true">collapse</el-radio-button>
</el-radio-group> -->
<el-menu :default-active="activeIndex" class="el-menu-part" router :collapse="isCollapse">
<el-menu-item index="/">
<el-icon>
<House />
</el-icon>
<el-icon><House /></el-icon>
<template #title><span>首页</span></template>
</el-menu-item>
<el-menu-item index="/alertManagement">
<el-icon>
<Management />
</el-icon>
<el-icon><Management /></el-icon>
<template #title><span>告警列表</span></template>
</el-menu-item>
<el-menu-item index="/dataStatistics">
<el-icon>
<TrendCharts />
</el-icon>
<el-icon><TrendCharts /></el-icon>
<template #title><span>数据统计</span></template>
</el-menu-item>
<el-menu-item index="/userList">
<el-icon>
<Avatar />
</el-icon>
<el-icon><Avatar /></el-icon>
<template #title><span>用户管理</span></template>
</el-menu-item>
<el-sub-menu index="1">
<template #title>
<el-icon>
<location />
</el-icon>
<el-icon><Location /></el-icon>
<span>面板测试</span>
</template>
<el-menu-item index="/test">
<el-icon>
<WarningFilled />
</el-icon>
<el-icon><WarningFilled /></el-icon>
<template #title><span>布局备份</span></template>
</el-menu-item>
<el-menu-item index="/alertChart">
<el-icon>
<WarningFilled />
</el-icon>
<el-icon><WarningFilled /></el-icon>
<template #title><span>功能点1测试</span></template>
</el-menu-item>
<el-menu-item index="/statistics">
<el-icon>
<document />
</el-icon>
<el-icon><Document /></el-icon>
<template #title><span>功能点2测试</span></template>
</el-menu-item>
<el-menu-item index="/cameras">
<el-icon>
<document />
</el-icon>
<el-icon><Document /></el-icon>
<template #title><span>功能点3测试</span></template>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<div class="content-layout">
<!-- 头部区域 -->
<el-header class="nav-header">
<!-- 收缩/展开按钮 -->
<el-icon @click="toggleCollapse" style="cursor: pointer; margin-right: 20px;">
<component :is="isCollapse ? Expand : Fold" />
</el-icon>
<div class="header-right">
<!-- 用户头像下拉菜单 -->
@ -81,14 +65,10 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-icon>
<User />
</el-icon>
<el-icon><User /></el-icon>
</el-dropdown-item>
<el-dropdown-item @click="onLogout">
<el-icon>
<SwitchButton />
</el-icon> 退
<el-icon><SwitchButton /></el-icon> 退
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -102,9 +82,7 @@
</el-main>
<!-- 页脚区域 -->
<el-footer class="nav-footer">
Powered by AI
</el-footer>
<el-footer class="nav-footer">Powered by AI</el-footer>
</div>
</div>
</template>
@ -112,19 +90,9 @@
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
Document,
WarningFilled,
Location,
User,
SwitchButton,
WarnTriangleFilled,
Setting,
House,
Grid,
Management,
TrendCharts,
Avatar
import {
Document, WarningFilled, Location, User, SwitchButton,
House, Management, TrendCharts, Avatar, Fold, Expand
} from '@element-plus/icons-vue';
import { BoxApi } from '@/utils/boxApi';
import { ElMessage } from 'element-plus';
@ -132,24 +100,14 @@ import { ElMessage } from 'element-plus';
const route = useRoute();
const router = useRouter();
const activeIndex = ref(route.path);
const isCollapse = ref(false);
const isCollapse = ref(false); //
const username = ref(''); //
// username
const username = ref(''); //
// localStorage
onMounted(() => {
const storedUsername = localStorage.getItem('username');
if (storedUsername) {
username.value = storedUsername;
} else {
username.value = '用户'; // localStorage ""
}
username.value = storedUsername ? storedUsername : '用户';
});
const apiInstance = new BoxApi();
// active
watch(
() => route.path,
(newPath) => {
@ -157,21 +115,20 @@ watch(
}
);
// const onLogout = () => {
// localStorage.removeItem('alertToken');
// router.push('/login');
// };
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value; //
};
const onLogout = async () => {
try {
localStorage.removeItem('alertToken');
ElMessage.success('退出登录成功');
router.push('/login');
await apiInstance.logout();
await new BoxApi().logout();
} catch (error: any) {
ElMessage.error(`后台接口调用失败: ${error.message}`);
}
};
</script>
<style scoped>
@ -179,19 +136,16 @@ const onLogout = async () => {
display: flex;
height: 100%;
width: 100%;
margin: 1px;
padding: 1px;
/* overflow: hidden; */
}
.nav-sidebar {
width: 150px;
background-color: #001529;
/* width: 200px; */
color: #fff;
min-height: 97.5vh;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
min-height: 100vh;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
transition: width 1s ease;
}
.logo {
@ -204,9 +158,7 @@ const onLogout = async () => {
background-color: transparent;
color: #fff;
border: 1px;
/* width: 200px; */
}
.el-menu-part .el-menu-item {
/* padding:30px 0; */
padding: 0px;
@ -218,8 +170,6 @@ const onLogout = async () => {
background-color: #001529;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* 悬停样式 */
.el-menu-part .el-menu-item:hover {
background-color: #001529;
@ -244,29 +194,27 @@ const onLogout = async () => {
}
/* 内容布局区域 */
.content-layout {
display: flex;
flex-direction: column;
flex-grow: 1;
max-height: 100%;
/* overflow-x: auto; */
/* min-width: 0; */
}
/* 头部样式 */
.nav-header {
/* background-color: #001529; */
background: linear-gradient(to right, rgba(0, 21, 41, 1), rgb(2, 16, 99,0.9));
background: linear-gradient(to right, rgba(0, 21, 41, 1), rgba(2, 16, 99, 0.9));
padding: 10px;
font-size: 18px;
font-weight: bold;
color: white;
height: 58px;
height: 68px;
display: flex;
justify-content: space-between;
align-items: center;
justify-content: right;
max-width: 99.9%;
/* flex-shrink: 1; */
/* justify-content: right; */
/* max-width: 100%; */
}
.header-right {
@ -277,32 +225,30 @@ const onLogout = async () => {
.username {
margin-left: 10px;
margin-right: 30px;
font-weight: bold;
font-size: 16px;
/* font-weight: bold;
font-size: 16px; */
color: #fff;
}
/* 主内容样式 */
.main-content {
background-color: #f5f8fc;
flex-grow: 1;
max-height: 90%;
max-height: 95vh;
margin: 0;
padding: 0;
width: 100%;
overflow-y: hidden;
}
/* 页脚样式 */
.nav-footer {
background-color: #fff;
/* padding: 10px; */
font-size: 16px;
color: #333;
height: 2vh;
height: 5vh;
display: flex;
justify-content: center;
text-align: center;
margin: 0;
padding: 0;
/* flex-shrink: 0; */
}
</style>

View File

@ -117,7 +117,7 @@ const initChart = () => {
name: '告警类型',
type: 'pie',
radius: '50%',
center: ['50%', '150px'],
center: ['50%', '50%'],
data: seriesData.value,
data: [],
emphasis: {
@ -177,8 +177,8 @@ onMounted(async () => {
/* background: linear-gradient(to top, rgba(16, 84, 194, 0.6), rgba(31, 48, 207, 0.7)); */
color: #fff;
border-radius: 8px;
margin: 0;
padding: 0;
/* margin: 10px; */
/* padding: 10px; */
/* height: 100vh; */
}
@ -190,13 +190,15 @@ onMounted(async () => {
}
.stats-row {
margin-top: 20px;
margin-bottom: 10px;
margin-top: 10px;
margin-bottom: 34px;
}
.chart {
width: 100%;
height: 380px;
/* min-height: 365px; */
height: 41vh;
min-width: 40vw;
/* height: 445px; */
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="alert-container">
<div class="alert-container" >
<el-row class="bottom-pan">
<el-col :span="24" class="panel-bottom">
<el-col class="panel-bottom">
<Cameras/>
</el-col>
</el-row>
@ -20,7 +20,7 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted,computed, onBeforeUnmount} from 'vue';
import Statistics from '@/components/Statistics.vue';
import AlertChart from '@/components/AlertChart.vue';
import Cameras from '@/components/Cameras.vue';
@ -30,6 +30,19 @@ import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
const boxApi = new BoxApi();
const typeMapping = reactive({});
const token = ref(null);
// const scale = ref(1);
// const scaleStyle = computed(() => ({
// transform: `scale(${scale.value})`,
// transformOrigin: 'top left',
// width: `${100 / scale.value}%`,
// }));
// const handleResize = () => {
// const clientWidth = document.documentElement.clientWidth;
// const scaleFactor = clientWidth / 1920;
// scale.value = scaleFactor < 1 ? scaleFactor : 1;
// };
//
const fetchTypeMapping = async (token) => {
@ -47,30 +60,45 @@ const fetchTypeMapping = async (token) => {
onMounted(async () => {
token.value = localStorage.getItem('alertToken');
await fetchTypeMapping(token.value);
// handleResize();
// window.addEventListener('resize', handleResize);
});
// onBeforeUnmount(() => {
// window.removeEventListener('resize', handleResize);
// });
</script>
<style scoped>
.alert-container {
/* transform: scale(0.97); */
padding: 0;
margin: 0;
background-color: #f5f7fa;
overflow-x: hidden;
height: 100vh;
width: 100%;
}
.top-pan {
/* padding: 10px; */
/* margin-bottom: 10px; */
display: flex;
gap: 5px;
/* display: flex; */
/* gap: 5px; */
background-color: #fff;
overflow: hidden;
/* overflow: hidden; */
/* height: 55vh; */
/* max-height: 450px; */
/* padding-left: 1vh; */
/* padding-right:1vh ; */
/* overflow: hidden; */
height: 55vh;
padding-left: 10px;
padding-right:10px ;
margin-top: 60px;
}
.bottom-pan{
margin: 0;
padding: 0;
height: 33vh;
}
.panel-top-left {
@ -83,7 +111,7 @@ onMounted(async () => {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
/* gap: 20px; */
}
.panel-bottom{

View File

@ -30,14 +30,8 @@
<canvas v-show="playing" ref="canvasRef" class="camera-large"></canvas>
<!-- 播放按钮 -->
<el-button
v-show="!playing || showButton"
class="play-button"
type="primary"
circle
size="large"
@click="handlePlayPause"
>
<el-button v-show="!playing || showButton" class="play-button" type="primary" circle size="large"
@click="handlePlayPause">
<el-icon>
<VideoPlay v-if="!playing" />
<VideoPause v-if="playing" />
@ -107,16 +101,16 @@ const handleStartStream = async () => {
playing.value = true;
//
const url = `ws://192.168.28.33:${streamPort.value}/`;
const url = `ws://192.168.28.33:${streamPort.value}/`;
console.log('Playing set to true:', playing.value);
if (playerRef.value) {
playerRef.value.destroy(); //
}
if (window.JSMpeg) {
playerRef.value = new window.JSMpeg.Player(url, {
canvas: canvasRef.value,
canvas: canvasRef.value,
autoplay: true,
videoBufferSize: 15 * 1024 * 1024,
audioBufferSize: 5 * 1024 * 1024,
@ -170,6 +164,7 @@ onMounted(() => {
.camera-list {
width: 20%;
min-width: 215px;
max-height: 90vh;
/* 限制高度为一屏 */
overflow-y: auto;
@ -268,20 +263,4 @@ onMounted(() => {
.play-button:hover {
opacity: 1;
}
.camera-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 10px;
width: 80%;
height: 90vh;
}
.camera-box {
position: relative;
background-color: #1a1a1a;
}
</style>

View File

@ -36,7 +36,7 @@
<el-col :span="24">
<div class="table-part">
<el-table v-loading="loading" :data="tableData"
class="user-table" border>
class="user-table" border max-height="700px">
<el-table-column v-if="getColumnVisibility('selection')" type="selection"
min-width="55"></el-table-column>
<el-table-column v-if="getColumnVisibility('id')" prop="id" label="序号"
@ -259,7 +259,8 @@ const handleDeleteSelected = () => {
<style scoped>
.user-list {
width: auto;
/* width: 100%; */
/* height: 100%; */
overflow: auto;
/* min-height: 650px; */
/* height: 100vh; */
@ -267,7 +268,9 @@ const handleDeleteSelected = () => {
}
.user-table{
width: 100%;
overflow: auto;
overflow-x: auto;
}
.header {