Compare commits
15 Commits
80738cb2a0
...
688658d1c2
Author | SHA1 | Date |
---|---|---|
|
688658d1c2 | |
|
9a6753cfa1 | |
|
76dfe72832 | |
|
31ece3ea1f | |
|
677804fbf6 | |
|
8acedde40f | |
|
d423ea00c0 | |
|
c76d2e498c | |
|
376981cf5e | |
|
d580f303d0 | |
|
e307cd7e94 | |
|
12676a3706 | |
|
631be0cbbd | |
|
b5f8e0e89f | |
|
b5044b700f |
|
@ -9,6 +9,7 @@
|
|||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.7.8",
|
||||
"vue": "^3.4.29",
|
||||
|
@ -1074,9 +1075,9 @@
|
|||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.12",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.12.tgz",
|
||||
"integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.7.8",
|
||||
"vue": "^3.4.29",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,5 @@
|
|||
body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
}
|
|
@ -3,189 +3,298 @@
|
|||
<el-card class="alert-card">
|
||||
<div class="alert-header">告警趋势</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="前一天" name="first">前一天数据</el-tab-pane>
|
||||
<el-tab-pane label="3天" name="second">近3天数据展示</el-tab-pane>
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div id="alertChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { login, getEvents } from '@/utils/superbox.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 使用 BoxApi
|
||||
|
||||
const username = 'turingvideo';
|
||||
const password = '1234qwer!';
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0)); // 初始化24小时的计数器
|
||||
const dailyCounts = ref(Array(7).fill(0)); // 初始化7天的计数器
|
||||
const weekDates = ref([]); // 存储前7天的日期
|
||||
const chartInstanceToday = ref(null);
|
||||
const chartInstanceWeek = ref(null);
|
||||
const todayLineChartDom = ref(null);
|
||||
const weekLineChartDom = ref(null);
|
||||
|
||||
export default {
|
||||
name: 'AlertChart',
|
||||
data() {
|
||||
return {
|
||||
activeName: 'first',
|
||||
eventsData: []
|
||||
const apiInstance = new BoxApi(); // 创建 BoxApi 实例
|
||||
|
||||
// 计算前7天的日期
|
||||
const calculateWeekDates = () => {
|
||||
const today = dayjs();
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 初始化本周图表
|
||||
const initWeekChart = () => {
|
||||
if (weekLineChartDom.value) {
|
||||
chartInstanceWeek.value = echarts.init(weekLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '最近7天告警数量趋势',
|
||||
top: '5%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value } = params[0];
|
||||
return `告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
data: weekDates.value.length > 0 ? weekDates.value : Array.from({ length: 7 }, (_, i) => `日期 ${i+1}`),
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
interval: 0,
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value.length > 0 ? dailyCounts.value : Array(7).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const token = await login(username, password);
|
||||
this.eventsData = await getEvents(token);
|
||||
this.initChart();
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(tab, event) {
|
||||
this.initChart();
|
||||
},
|
||||
initChart() {
|
||||
var chartDom = document.getElementById('alertChart');
|
||||
var myChart = echarts.init(chartDom);
|
||||
var option;
|
||||
|
||||
if (this.activeName === 'first') {
|
||||
option = this.get24HourChartOption();
|
||||
} else if (this.activeName === 'second') {
|
||||
option = this.get3DayChartOption();
|
||||
chartInstanceWeek.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化今日图表
|
||||
const initTodayChart = () => {
|
||||
if (todayLineChartDom.value) {
|
||||
chartInstanceToday.value = echarts.init(todayLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '今日告警数量趋势',
|
||||
top: '3%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value, name } = params[0];
|
||||
return `${name}<br/>告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`),
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
color: '#fff',
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value.length > 0 ? hourlyCounts.value : Array(24).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
chartInstanceToday.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 分批获取并处理告警数据
|
||||
const fetchAndProcessEvents = async (token) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 2000; // 每次加载100条数据
|
||||
let allEvents = [];
|
||||
|
||||
// 第一次请求,获取告警总数
|
||||
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents = [...firstBatch];
|
||||
|
||||
// 根据告警总数逐页加载所有数据
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
|
||||
// 循环分页加载剩余的数据
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents = [...allEvents, ...nextBatch];
|
||||
|
||||
// 每次加载数据后逐步更新图表
|
||||
processEventData(allEvents);
|
||||
}
|
||||
|
||||
// 最终处理全部数据
|
||||
processEventData(allEvents);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理告警数据并更新图表
|
||||
const processEventData = (events) => {
|
||||
// 重置计数器
|
||||
hourlyCounts.value = Array(24).fill(0);
|
||||
dailyCounts.value = Array(7).fill(0);
|
||||
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
// 统计数据
|
||||
events.forEach(event => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDate = new Date(endedAt);
|
||||
|
||||
// 统计今日每小时的告警数量
|
||||
if (endedDate >= startOfToday && endedDate < endOfToday) {
|
||||
const hour = endedDate.getHours();
|
||||
hourlyCounts.value[hour] += 1;
|
||||
}
|
||||
|
||||
option && myChart.setOption(option);
|
||||
},
|
||||
get24HourChartOption() {
|
||||
const now = new Date();
|
||||
const todayMidnight = new Date(now);
|
||||
todayMidnight.setHours(0, 0, 0, 0);
|
||||
|
||||
const yesterdayMidnight = new Date(todayMidnight);
|
||||
yesterdayMidnight.setDate(todayMidnight.getDate() - 1);
|
||||
|
||||
const timePoints = ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'];
|
||||
const alertCounts = Array(timePoints.length).fill(0);
|
||||
|
||||
this.eventsData.forEach(event => {
|
||||
const eventTime = new Date(event.started_at);
|
||||
if (eventTime >= yesterdayMidnight && eventTime < todayMidnight) {
|
||||
const hours = eventTime.getHours();
|
||||
if (hours < 4) alertCounts[1]++;
|
||||
else if (hours < 8) alertCounts[2]++;
|
||||
else if (hours < 12) alertCounts[3]++;
|
||||
else if (hours < 16) alertCounts[4]++;
|
||||
else if (hours < 20) alertCounts[5]++;
|
||||
else if (hours < 24) alertCounts[6]++;
|
||||
// 统计最近7天的告警数量
|
||||
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||
weekDates.value.forEach((date, index) => {
|
||||
if (endedDay === date) {
|
||||
dailyCounts.value[index] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Accumulate the alert counts
|
||||
for (let i = 1; i < alertCounts.length; i++) {
|
||||
alertCounts[i] += alertCounts[i - 1];
|
||||
}
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: '24小时内客流数量变化',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: '{b0}: {c0}'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: timePoints
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: alertCounts,
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
get3DayChartOption() {
|
||||
const now = new Date();
|
||||
const day1Midnight = new Date(now);
|
||||
day1Midnight.setHours(0, 0, 0, 0);
|
||||
|
||||
const day2Midnight = new Date(day1Midnight);
|
||||
day2Midnight.setDate(day1Midnight.getDate() - 1);
|
||||
|
||||
const day3Midnight = new Date(day2Midnight);
|
||||
day3Midnight.setDate(day2Midnight.getDate() - 1);
|
||||
|
||||
// 在生成字符串之前增加一天
|
||||
const adjustedDay1 = new Date(day1Midnight);
|
||||
adjustedDay1.setDate(day1Midnight.getDate() + 1);
|
||||
|
||||
const adjustedDay2 = new Date(day2Midnight);
|
||||
adjustedDay2.setDate(day2Midnight.getDate() + 1);
|
||||
|
||||
const adjustedDay3 = new Date(day3Midnight);
|
||||
adjustedDay3.setDate(day3Midnight.getDate() + 1);
|
||||
|
||||
// 使用调整后的日期作为时间点
|
||||
const timePoints = [
|
||||
adjustedDay3.toISOString().slice(0, 10),
|
||||
adjustedDay2.toISOString().slice(0, 10),
|
||||
adjustedDay1.toISOString().slice(0, 10),
|
||||
];
|
||||
const alertCounts = Array(timePoints.length).fill(0);
|
||||
|
||||
this.eventsData.forEach(event => {
|
||||
const eventTime = new Date(event.started_at);
|
||||
if (eventTime >= day3Midnight && eventTime < day2Midnight) {
|
||||
alertCounts[0]++;
|
||||
} else if (eventTime >= day2Midnight && eventTime < day1Midnight) {
|
||||
alertCounts[1]++;
|
||||
} else if (eventTime >= day1Midnight && eventTime <= now) {
|
||||
alertCounts[2]++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: '近3天内告警数量变化',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: '{b0}: {c0}'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: timePoints
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: alertCounts,
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 更新图表
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.setOption({
|
||||
series: [{ data: hourlyCounts.value }]
|
||||
});
|
||||
}
|
||||
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.setOption({
|
||||
series: [{ data: dailyCounts.value }]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates(); // 计算前7天日期
|
||||
|
||||
const token = localStorage.getItem('alertToken');
|
||||
await fetchAndProcessEvents(token);
|
||||
|
||||
// 初始化图表
|
||||
initTodayChart();
|
||||
initWeekChart();
|
||||
});
|
||||
|
||||
// 监听hourlyCounts,当其变化时更新今日图表
|
||||
watch(hourlyCounts, () => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.setOption({
|
||||
series: [{
|
||||
data: hourlyCounts.value
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 监听dailyCounts,当其变化时更新本周图表
|
||||
watch(dailyCounts, () => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.setOption({
|
||||
series: [{
|
||||
data: dailyCounts.value
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// tab点击处理函数
|
||||
const handleClick = (tab) => {
|
||||
if (tab.name === 'first') {
|
||||
initTodayChart(); // 切换到今日,重新加载今日图表
|
||||
} else if (tab.name === 'second') {
|
||||
initWeekChart(); // 切换到本周,重新加载本周图表
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -194,29 +303,28 @@ export default {
|
|||
.alert-card {
|
||||
background-color: #2a3f54;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.alert-tabs {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
color: #fff;
|
||||
width: 780px;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
|
|
|
@ -1,34 +1,161 @@
|
|||
<template>
|
||||
<div id="layout" class="layout-container">
|
||||
<el-aside width="200px" class="nav-sidebar">
|
||||
<el-aside class="nav-sidebar">
|
||||
<div class="logo">
|
||||
<el-image src="/logo.png" fit="contain" />
|
||||
<el-image src="/turing.png" fit="contain" />
|
||||
</div>
|
||||
<el-menu :default-active="activeIndex" class="el-menu-part" router>
|
||||
<el-menu-item index="/">告警管理</el-menu-item>
|
||||
<el-menu-item index="/test">测试</el-menu-item>
|
||||
<!-- <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>
|
||||
<template #title><span>首页</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/alertManagement">
|
||||
<el-icon>
|
||||
<Management />
|
||||
</el-icon>
|
||||
<template #title><span>告警列表</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/dataStatistics">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<template #title><span>数据统计</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/userList">
|
||||
<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>
|
||||
<span>面板测试</span>
|
||||
</template>
|
||||
<el-menu-item index="/alertChart">
|
||||
<el-icon>
|
||||
<WarningFilled />
|
||||
</el-icon>
|
||||
<template #title><span>功能点1测试</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/statistics">
|
||||
<el-icon>
|
||||
<document />
|
||||
</el-icon>
|
||||
<template #title><span>功能点2测试</span></template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<div class="content-layout">
|
||||
<!-- 头部区域 -->
|
||||
<el-header class="nav-header">
|
||||
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
<div class="header-right">
|
||||
<!-- 用户头像下拉菜单 -->
|
||||
<el-dropdown trigger="click">
|
||||
<span class="el-dropdown-link">
|
||||
<el-avatar shape="square"> 用户 </el-avatar>
|
||||
<span class="username">{{ username }}</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon> 用户中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="onLogout">
|
||||
<el-icon>
|
||||
<SwitchButton />
|
||||
</el-icon> 退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
|
||||
<!-- 页脚区域 -->
|
||||
<el-footer class="nav-footer">
|
||||
Powered by AI
|
||||
</el-footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
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
|
||||
} from '@element-plus/icons-vue';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const activeIndex = ref(route.path);
|
||||
const isCollapse = ref(false);
|
||||
|
||||
// 动态获取 username
|
||||
const username = ref(''); // 默认值为空
|
||||
|
||||
// 页面加载时,检查 localStorage 是否存储了用户名
|
||||
onMounted(() => {
|
||||
const storedUsername = localStorage.getItem('username');
|
||||
if (storedUsername) {
|
||||
username.value = storedUsername;
|
||||
} else {
|
||||
username.value = '用户'; // 如果 localStorage 没有值,默认显示 "用户"
|
||||
}
|
||||
});
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
// 监听路由变化,更新菜单的 active 项
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
activeIndex.value = newPath;
|
||||
}
|
||||
);
|
||||
|
||||
// const onLogout = () => {
|
||||
// localStorage.removeItem('alertToken');
|
||||
// router.push('/login');
|
||||
// };
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
localStorage.removeItem('alertToken');
|
||||
ElMessage.success('退出登录成功');
|
||||
router.push('/login');
|
||||
await apiInstance.logout();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(`后台接口调用失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -37,14 +164,18 @@ watch(
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 1px;
|
||||
/* padding: 1px; */
|
||||
|
||||
padding: 1px;
|
||||
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
background-color: #1f2d3d;
|
||||
width: 200px;
|
||||
background-color: #001529;
|
||||
/* width: 200px; */
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
@ -68,14 +199,86 @@ watch(
|
|||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background-color: #001529;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
/* 悬停样式 */
|
||||
.el-menu-part .el-menu-item:hover {
|
||||
background-color: #001529;
|
||||
color: #00aaff;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
/* 选中样式 */
|
||||
.el-menu-part .el-menu-item.is-active {
|
||||
background-color: #001529;
|
||||
color: #eeea07;
|
||||
}
|
||||
</style>
|
||||
|
||||
::v-deep .el-sub-menu__title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
::v-deep .el-sub-menu__title:hover {
|
||||
background-color: #001529;
|
||||
color: #00aaff;
|
||||
}
|
||||
|
||||
|
||||
/* 内容布局区域 */
|
||||
.content-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.nav-header {
|
||||
background-color: #001529;
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
/* flex-shrink: 1; */
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 主内容样式 */
|
||||
.main-content {
|
||||
background-color: #f5f8fc;
|
||||
flex-grow: 1;
|
||||
min-height: 80%;
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
.nav-footer {
|
||||
background-color: #fff;
|
||||
/* padding: 10px; */
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
height: 2vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
/* flex-shrink: 0; */
|
||||
}</style>
|
||||
|
|
|
@ -11,126 +11,184 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { login, getAlgorithms,getEvents} from '@/utils/superbox.js';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 引入 BoxApi 类
|
||||
|
||||
const username = 'turingvideo';
|
||||
const password = '1234qwer!';
|
||||
// 响应式数据
|
||||
const chart = ref(null);
|
||||
const seriesData = ref([]);
|
||||
|
||||
// BoxApi 实例
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
export default {
|
||||
name: 'Statistics',
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
seriesData: [
|
||||
// 获取告警类型的映射
|
||||
const fetchTypeMapping = async (token) => {
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
let mapping = algorithms.map(algorithm => ({
|
||||
value: 0,
|
||||
code_name: algorithm.code_name,
|
||||
name: algorithm.name
|
||||
}));
|
||||
|
||||
],
|
||||
};
|
||||
},
|
||||
// mounted() {
|
||||
// this.initChart();
|
||||
// },
|
||||
async created(){
|
||||
try{
|
||||
const token = await login(username, password);
|
||||
await this.fetchTypeMapping(token);
|
||||
await this.updateSeriesData(token);
|
||||
this.initChart();
|
||||
}catch(error){
|
||||
console.error("Error fetching algorithms:", error);
|
||||
// 添加额外的类型
|
||||
const newMapping = [
|
||||
{ code_name: "minizawu:532", name: "杂物堆积", value: 0 }
|
||||
];
|
||||
|
||||
seriesData.value = mapping.concat(newMapping);
|
||||
};
|
||||
|
||||
// 分批次获取全量告警数据并更新 seriesData
|
||||
const fetchAndProcessEvents = async (token) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 2000; // 每次加载 2000 条
|
||||
let allEvents = [];
|
||||
|
||||
// 第一次请求,获取告警总数和首批数据
|
||||
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents = [...firstBatch];
|
||||
|
||||
// 根据告警总数计算总页数
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
|
||||
// 循环分页加载剩余的数据
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents = [...allEvents, ...nextBatch];
|
||||
|
||||
// 每次加载数据后逐步更新图表
|
||||
processEventData(allEvents);
|
||||
updateChart(); // 逐步更新图表
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchTypeMapping(token) {
|
||||
const algorithms = await getAlgorithms(token);
|
||||
let mapping = algorithms.map(algorithm => ({
|
||||
value: 0, // 初始化为10,可以根据实际数据进行调整
|
||||
code_name: algorithm.code_name,
|
||||
name: algorithm.name
|
||||
}));
|
||||
const newMapping = [
|
||||
{code_name: "minizawu:532",name: "杂物堆积",value: 0},
|
||||
]
|
||||
|
||||
mapping = mapping.concat(newMapping);
|
||||
this.seriesData = mapping;
|
||||
},
|
||||
async updateSeriesData(token){
|
||||
const events = await getEvents(token);
|
||||
events.forEach(event => {
|
||||
const matchAlgorithm = this.seriesData.find(item => item.code_name === event.types);
|
||||
if (matchAlgorithm){
|
||||
matchAlgorithm.value += 1;
|
||||
}
|
||||
})
|
||||
},
|
||||
initChart() {
|
||||
this.chart = echarts.init(this.$refs.chart);
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
itemGap: 20,
|
||||
data: this.seriesData.map(item => item.name),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警类型',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
center: ['50%', '150px'],
|
||||
data: this.seriesData,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: true
|
||||
},
|
||||
stillShowZeroSum: false,
|
||||
}
|
||||
]
|
||||
};
|
||||
this.chart.setOption(option);
|
||||
}
|
||||
// 最终处理全部数据
|
||||
processEventData(allEvents);
|
||||
updateChart(); // 最终更新图表
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理告警事件数据并更新 seriesData
|
||||
const processEventData = (events) => {
|
||||
// 重置数据,防止累计错误
|
||||
seriesData.value.forEach(item => {
|
||||
item.value = 0;
|
||||
});
|
||||
|
||||
// 遍历事件并统计告警类型数量
|
||||
events.forEach(event => {
|
||||
const matchAlgorithm = seriesData.value.find(item => item.code_name === event.types);
|
||||
if (matchAlgorithm) {
|
||||
matchAlgorithm.value += 1;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
// 初始化 ECharts 实例
|
||||
if (!chart.value) {
|
||||
console.error("Chart DOM element is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
chart.value = echarts.init(chart.value);
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
itemGap: 20,
|
||||
data: seriesData.value.map(item => item.name),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警类型',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
center: ['50%', '150px'],
|
||||
data: seriesData.value,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
stillShowZeroSum: false,
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.value.setOption(option);
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = () => {
|
||||
if (chart.value && typeof chart.value.setOption === 'function') {
|
||||
chart.value.setOption({
|
||||
series: [
|
||||
{
|
||||
data: seriesData.value
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
console.error("ECharts instance is not initialized properly");
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
|
||||
// 获取告警类型映射
|
||||
await fetchTypeMapping(token);
|
||||
|
||||
// 初始化图表
|
||||
initChart();
|
||||
|
||||
// 分批次获取告警数据并更新图表
|
||||
await fetchAndProcessEvents(token);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stats-card {
|
||||
background-color: #2a3f54;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
/* margin-top: 20px; */
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
height: 445px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,28 +1,42 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<el-row>
|
||||
<el-col :span="16">
|
||||
<div class="alert-container">
|
||||
<el-row class="top-pan">
|
||||
<el-col :sm="24" :md="12" class="panel-section">
|
||||
<statistics />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" class="panel-section">
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="middle-row">
|
||||
<span>
|
||||
告警总数:{{ totalItems }}
|
||||
</span>
|
||||
</el-row>
|
||||
<el-row class="table-row">
|
||||
<el-col :span="24">
|
||||
<div class="table-container">
|
||||
<el-table :data="paginatedData">
|
||||
<el-table-column prop="id" label="告警编号" width="140"></el-table-column>
|
||||
<el-table-column label="告警类型" width="140">
|
||||
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header"
|
||||
:fit="true">
|
||||
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
||||
<el-table-column label="告警类型" min-width="150">
|
||||
<template v-slot="scope">
|
||||
<!-- {{ typeMapping[scope.row.types] || codenameTranslate(scope.row.types) }} -->
|
||||
{{ typeMapping[scope.row.types] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="camera.name" label="告警位置" width="180"></el-table-column>
|
||||
<el-table-column label="告警时间" width="180">
|
||||
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
||||
<el-table-column label="告警时间" min-width="200">
|
||||
<template v-slot="scope">
|
||||
{{ formatDateTime(scope.row.started_at) }}
|
||||
{{ formatDateTime(scope.row.ended_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="告警状态" width="140">
|
||||
<el-table-column prop="status" label="告警状态" min-width="100">
|
||||
<template v-slot="scope">
|
||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{ statusMapping[scope.row.status] }}</el-tag>
|
||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{ statusMapping[scope.row.status]
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140">
|
||||
<el-table-column label="操作" min-width="100">
|
||||
<template v-slot="scope">
|
||||
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
||||
</template>
|
||||
|
@ -30,23 +44,11 @@
|
|||
</el-table>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="tableData.length"
|
||||
:page-size="pageSize" :current-page.sync="currentPage" @current-change="handlePageChange"
|
||||
@size-change="handleSizeChange">
|
||||
<el-pagination @size-change="handleSizeChange" @current-change="handlePageChange" :current-page="currentPage"
|
||||
:page-size="pageSize" :total="totalItems" layout="total, sizes, prev, pager, next, jumper">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<div class="right-panel">
|
||||
<div class="panel-section">
|
||||
<statistics />
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<alertChart />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
||||
<div>
|
||||
|
@ -55,7 +57,6 @@
|
|||
<p>告警编号: {{ selectedRow.id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<!-- <p>告警类型: {{ typeMapping[selectedRow.types] || codenameTranslate(selectedRow.types) }}</p> -->
|
||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
|
@ -67,7 +68,8 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{ statusMapping[selectedRow.status]
|
||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[selectedRow.status]
|
||||
}}</el-tag></p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
|
@ -100,222 +102,180 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import Statistics from '@/components/Statistics.vue';
|
||||
import AlertChart from '@/components/AlertChart.vue';
|
||||
import { login, getEvents, initCodeNameMap, codenameTranslate, getAlgorithms } from '@/utils/superbox.js';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
const username = 'turingvideo';
|
||||
const password = '1234qwer!';
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
|
||||
export default {
|
||||
name: 'AlertManagement',
|
||||
components: {
|
||||
Statistics,
|
||||
AlertChart,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tableData: [],
|
||||
dialogVisible: false,
|
||||
selectedRow: {},
|
||||
mediums: {},
|
||||
duration: '',
|
||||
typeMapping: {
|
||||
// 'abnormal:525': '黑屏检测',
|
||||
// 'car_blocking:512': '车辆拥塞',
|
||||
// 'car_blocking:514': '违规停车',
|
||||
// 'escalator_status:518': '扶梯运行状态',
|
||||
// 'gathering:520': '人员密集',
|
||||
// 'gathering:521': '保洁点名',
|
||||
// 'intrude:513': '入侵检测',
|
||||
// 'long_term_door_status:526': '长期门状态检测',
|
||||
// 'lying_down:527': '人员倒地',
|
||||
// 'minizawu:531': '杂物堆积',
|
||||
// 'minizawu:532': '杂物堆积',
|
||||
// 'personnel_stay:535': '人员逗留',
|
||||
// 'vacant:524': '人员离岗',
|
||||
// 'zawu:516': '饮料垃圾检测',
|
||||
// 'zawu:517': '垃圾桶满溢',
|
||||
// 'zawu:519': '违规放置',
|
||||
// 'zawu:523': '绿化带垃圾检测',
|
||||
},
|
||||
statusMapping: {
|
||||
'pending': '待处理',
|
||||
'closed': '已处理'
|
||||
},
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
// 响应式数据
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const selectedRow = ref({});
|
||||
const mediums = ref([]);
|
||||
const duration = ref('');
|
||||
const typeMapping = reactive({});
|
||||
const statusMapping = {
|
||||
'pending': '待处理',
|
||||
'closed': '已处理'
|
||||
};
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const totalItems = ref(0);
|
||||
const token = ref(null);
|
||||
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const token = await login(username, password);
|
||||
// await initCodeNameMap(token);
|
||||
this.tableData = await getEvents(token);
|
||||
this.typeMapping = await this.fetchTypeMapping(token);
|
||||
console.log(this.tableData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
paginatedData() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
const end = start + this.pageSize;
|
||||
return this.tableData.slice(start, end);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchTypeMapping(token) {
|
||||
let algorithms = await getAlgorithms(token);
|
||||
const additionalMappings = [
|
||||
{ code_name: 'minizawu:532', name: '杂物堆积' },
|
||||
|
||||
];
|
||||
algorithms = algorithms.concat(additionalMappings);
|
||||
let mapping = {};
|
||||
algorithms.forEach((algorithm) => {
|
||||
mapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
|
||||
// console.log("mapping: ", mapping);
|
||||
return mapping;
|
||||
},
|
||||
|
||||
|
||||
handleView(row) {
|
||||
this.selectedRow = row;
|
||||
this.duration = this.calculateDuration(row.started_at, row.ended_at);
|
||||
row.formatted_started_at = this.formatDateTime(row.started_at);
|
||||
|
||||
// console.log("duration: ", row.started_at);
|
||||
// console.log("duration: ", row.ended_at);
|
||||
// console.log("duration: ", this.duration);
|
||||
this.dialogVisible = true;
|
||||
this.mediums = row.mediums || {};
|
||||
if (Array.isArray(row.mediums)) {
|
||||
row.mediums.forEach(medium => {
|
||||
// console.log("medium: ", medium.file);
|
||||
// console.log("medium: ", medium.id);
|
||||
});
|
||||
} else {
|
||||
// console.log("No mediums available");
|
||||
}
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
handleSizeChange(size) {
|
||||
this.pageSize = size;
|
||||
},
|
||||
calculateDuration(started_at, ended_at) {
|
||||
const start = new Date(started_at);
|
||||
const end = new Date(ended_at);
|
||||
const duration = end - start;
|
||||
const minutes = Math.floor(duration / 60000);
|
||||
const seconds = ((duration % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}分${(seconds < 10 ? '0' : '')}${seconds}秒`;
|
||||
},
|
||||
formatDateTime(datetime) {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
// 获取类型映射
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取告警数据
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
|
||||
tableData.value = data;
|
||||
totalItems.value = total;
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 查看告警详情
|
||||
const handleView = (row) => {
|
||||
selectedRow.value = row;
|
||||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||||
row.formatted_started_at = formatDateTime(row.started_at);
|
||||
dialogVisible.value = true;
|
||||
mediums.value = row.mediums || [];
|
||||
};
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// 页大小处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// 计算持续时间
|
||||
const calculateDuration = (started_at, ended_at) => {
|
||||
const start = new Date(started_at);
|
||||
const end = new Date(ended_at);
|
||||
const durationMs = end - start;
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 页面加载后执行
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
await fetchEvents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
padding: 40px;
|
||||
.alert-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.top-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.top-scroll-content {
|
||||
/* 高度滚动条显示出来默认值 */
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
width: 80%;
|
||||
max-height: 1000px;
|
||||
overflow-x: auto;
|
||||
padding-left: 7%;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
.top-pan {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 10px 10px 400px 10px;
|
||||
background-color: #f5f5f5;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
gap: 10px;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.middle-row {
|
||||
min-height: 5vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 弹窗> 显示左右等分*/
|
||||
.table-container {
|
||||
width: 100%;
|
||||
height: 380px;
|
||||
min-height: 60%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #f7f8fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
::v-deep .el-table th.el-table__cell {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.event-media {
|
||||
/* display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 20px; */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* 添加这个属性来左右等分 */
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.media-container {
|
||||
flex: 0 0 48%;
|
||||
/* 确保每块区域占据一行的48% */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 使内容在区域内居中 */
|
||||
margin-bottom: 20px;
|
||||
/* 添加底部间距,使多个元素之间有间隔 */
|
||||
box-sizing: border-box;
|
||||
/* 确保padding和border不会影响大小 */
|
||||
padding: 10px;
|
||||
/* 添加内边距,使内容不靠近边框 */
|
||||
}
|
||||
|
||||
.video-item video,
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
/* 保证视频和图片在容器内等宽 */
|
||||
height: auto;
|
||||
/* 保证视频和图片比例正常 */
|
||||
border-radius: 8px;
|
||||
/* 圆润效果 */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
/* 阴影效果 */
|
||||
}
|
||||
|
||||
.video-item p,
|
||||
|
@ -328,7 +288,12 @@ export default {
|
|||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
margin-right: 150px;
|
||||
background-color: #e8e9e4;
|
||||
}
|
||||
|
||||
::v-deep .pagination-container .el-pagination__total,
|
||||
::v-deep .pagination-container .el-pagination__goto,
|
||||
::v-deep .pagination-container .el-pagination__classifier {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
<template>
|
||||
<div class="alert-container">
|
||||
<el-row class="top-pan">
|
||||
</el-row>
|
||||
<el-row class="middle-row">
|
||||
<span>
|
||||
告警总数:{{ displayTotalItems }}
|
||||
</span>
|
||||
</el-row>
|
||||
<el-row class="table-row">
|
||||
<el-col :span="24">
|
||||
<div class="table-container">
|
||||
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header"
|
||||
:fit="true">
|
||||
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
||||
<el-table-column label="告警类型" min-width="150">
|
||||
<template v-slot="scope">
|
||||
{{ typeMapping[scope.row.types] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
||||
<el-table-column label="告警时间" min-width="200">
|
||||
<template v-slot="scope">
|
||||
{{ formatDateTime(scope.row.ended_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="告警状态" min-width="100">
|
||||
<template v-slot="scope">
|
||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[scope.row.status]
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="100">
|
||||
<template v-slot="scope">
|
||||
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<el-pagination @size-change="handleSizeChange" @current-change="handlePageChange"
|
||||
:current-page="currentPage" :page-size="pageSize" :total="totalItems"
|
||||
layout="total, sizes, prev, pager, next, jumper">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
<p>告警编号: {{ selectedRow.id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[selectedRow.status]
|
||||
}}</el-tag></p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>持续时间: {{ duration }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>备注: {{ selectedRow.note }}</p>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="event-media">
|
||||
<div v-for="medium in selectedRow.mediums" :key="medium.id" class="media-container">
|
||||
<div v-if="medium.name === 'video'" class="media-item video-item">
|
||||
<p>告警关联视频</p>
|
||||
<video :src="medium.file" controls></video>
|
||||
</div>
|
||||
<div v-if="medium.name === 'snapshot'" class="media-item image-item">
|
||||
<p>告警关联图片</p>
|
||||
<el-image :src="medium.file" fit="contain"></el-image>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<div class=""></div>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
|
||||
// 响应式数据
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const selectedRow = ref({});
|
||||
const mediums = ref([]);
|
||||
const duration = ref('');
|
||||
const typeMapping = reactive({});
|
||||
const statusMapping = {
|
||||
'pending': '待处理',
|
||||
'closed': '已处理'
|
||||
};
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const token = ref(null);
|
||||
const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0); // 用于展示的数字
|
||||
|
||||
// 获取类型映射
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取告警数据
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value);
|
||||
tableData.value = data;
|
||||
totalItems.value = total;
|
||||
animateNumberChange(total);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const animateNumberChange = (newTotal) => {
|
||||
const stepTime = 50; // 每次递增的时间间隔
|
||||
// const stepCount = Math.abs(newTotal - displayTotalItems.value);
|
||||
const stepCount = Math.abs(newTotal - displayTotalItems.value)/20;
|
||||
|
||||
const incrementStep = Math.ceil((newTotal - displayTotalItems.value) / stepCount); // 每次递增的值
|
||||
|
||||
const step = () => {
|
||||
if (displayTotalItems.value < newTotal) {
|
||||
displayTotalItems.value = Math.min(displayTotalItems.value + incrementStep, newTotal);
|
||||
requestAnimationFrame(step);
|
||||
} else if (displayTotalItems.value > newTotal) {
|
||||
displayTotalItems.value = Math.max(displayTotalItems.value - incrementStep, newTotal);
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
// 查看告警详情
|
||||
const handleView = (row) => {
|
||||
selectedRow.value = row;
|
||||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||||
row.formatted_started_at = formatDateTime(row.started_at);
|
||||
dialogVisible.value = true;
|
||||
mediums.value = row.mediums || [];
|
||||
};
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// 页大小处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// 计算持续时间
|
||||
const calculateDuration = (started_at, ended_at) => {
|
||||
const start = new Date(started_at);
|
||||
const end = new Date(ended_at);
|
||||
const durationMs = end - start;
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 页面加载后执行
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
await fetchEvents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
/* .top-pan {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
} */
|
||||
|
||||
/* .panel-section {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 15px;
|
||||
} */
|
||||
|
||||
.middle-row {
|
||||
min-height: 5vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
width: 100%;
|
||||
height: 380px;
|
||||
min-height: 60%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #f7f8fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
::v-deep .el-table th.el-table__cell {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.event-media {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.media-container {
|
||||
flex: 0 0 48%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.video-item video,
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.video-item p,
|
||||
.image-item p {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #e8e9e4;
|
||||
}
|
||||
|
||||
::v-deep .pagination-container .el-pagination__total,
|
||||
::v-deep .pagination-container .el-pagination__goto,
|
||||
::v-deep .pagination-container .el-pagination__classifier {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>
|
||||
首页
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
|
@ -1,148 +1,204 @@
|
|||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import { Lock, User } from '@element-plus/icons-vue';
|
||||
|
||||
import { useSuperbox } from '@/utils/Superbox'
|
||||
|
||||
import { useSetToken } from '@/utils/misc';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
import { User, Lock } from '@element-plus/icons-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const superbox = useSuperbox();
|
||||
const route = useRoute();
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
username: '',
|
||||
password: '',
|
||||
address: '',
|
||||
port: '',
|
||||
remember: false // 新增 "记住密码" 选项
|
||||
});
|
||||
|
||||
const setToken = useSetToken();
|
||||
const errorMessage = ref('');
|
||||
const username = ref(localStorage.getItem('username') || '');
|
||||
const alertToken = ref('');
|
||||
const cookless = ref(false);
|
||||
|
||||
// 创建实例
|
||||
let apiInstance: BoxApi | null = null;
|
||||
|
||||
// 加载页面时检查是否有记住的密码
|
||||
onMounted(() => {
|
||||
const savedUsername = localStorage.getItem('username');
|
||||
const savedPassword = localStorage.getItem('rememberedPassword');
|
||||
const savedAddress = localStorage.getItem('rememberedAddress');
|
||||
const savedPort = localStorage.getItem('rememberedPort');
|
||||
|
||||
if (savedUsername && savedPassword && savedAddress && savedPort) {
|
||||
loginForm.username = savedUsername;
|
||||
loginForm.password = savedPassword;
|
||||
loginForm.address = savedAddress;
|
||||
loginForm.port = savedPort;
|
||||
loginForm.remember = true; // 如果有记住的数据,将复选框设为选中状态
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const token = await superbox.login(loginForm.username, loginForm.password, false);
|
||||
if (token) {
|
||||
setToken(token);
|
||||
router.push('/');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage(
|
||||
{
|
||||
message: '用户名或密码错误,登录失败',
|
||||
type: 'error',
|
||||
duration: 5000
|
||||
}
|
||||
);
|
||||
console.log(error);
|
||||
try {
|
||||
// 检查服务器地址和端口是否填写
|
||||
if (!loginForm.address || !loginForm.port) {
|
||||
errorMessage.value = '请输入服务器地址和端口';
|
||||
return;
|
||||
}
|
||||
|
||||
// 实例化 SuperboxApi
|
||||
apiInstance = new BoxApi(loginForm.address, parseInt(loginForm.port));
|
||||
|
||||
// 调用 SuperboxApi 的 login 方法,传入 cookless 为 false
|
||||
const apiToken = await apiInstance.login(loginForm.username, loginForm.password,false);
|
||||
|
||||
// 保存 token 到本地存储
|
||||
localStorage.setItem('alertToken', apiToken);
|
||||
|
||||
// 如果勾选了“记住密码”,将用户名、密码、地址和端口存入 localStorage
|
||||
if (loginForm.remember) {
|
||||
localStorage.setItem('username', loginForm.username);
|
||||
localStorage.setItem('rememberedPassword', loginForm.password);
|
||||
localStorage.setItem('rememberedAddress', loginForm.address);
|
||||
localStorage.setItem('rememberedPort', loginForm.port);
|
||||
} else {
|
||||
// 如果未勾选“记住密码”,清除之前记住的数据
|
||||
localStorage.removeItem('rememberedPassword');
|
||||
localStorage.removeItem('rememberedAddress');
|
||||
localStorage.removeItem('rememberedPort');
|
||||
}
|
||||
|
||||
// 跳转到首页
|
||||
let redirect = route.query.redirect;
|
||||
if (Array.isArray(redirect)) {
|
||||
redirect = redirect[0]; // 如果 redirect 是数组,取第一个元素
|
||||
}
|
||||
router.push(redirect || '/'); // 确保 redirect 是字符串或使用默认的 '/'
|
||||
} catch (error) {
|
||||
// 捕获错误并显示错误信息
|
||||
errorMessage.value = '用户名或密码错误,或者连接服务器失败';
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
loginForm.username = '';
|
||||
loginForm.password = '';
|
||||
loginForm.username = '';
|
||||
loginForm.password = '';
|
||||
loginForm.address = '';
|
||||
loginForm.port = '';
|
||||
loginForm.remember = false; // 重置记住密码复选框
|
||||
errorMessage.value = '';
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
|
||||
<div class="login-form">
|
||||
<div class="login-header">
|
||||
<el-row justify="center">
|
||||
<el-image src="./logo.png" fit="contain" />
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="login-body">
|
||||
<el-form :model="loginForm" style="max-width: 600px;">
|
||||
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.username" placeholder="用户名" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.password" placeholder="密码" type="password" show-password clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-row justify="end">
|
||||
<el-form-item size="small" :gutter="30">
|
||||
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
||||
<el-button type="primary" @click="onSubmit">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<el-row justify="end">
|
||||
<el-text class="plain-text">Powered by TuringAI</el-text>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="main-layout">
|
||||
<div class="login-form">
|
||||
<div class="login-header">
|
||||
<el-row justify="center">
|
||||
<el-image src="/logo.png" fit="contain" />
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="login-body">
|
||||
<el-form :model="loginForm" style="max-width: 600px;">
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.address" placeholder="服务器地址" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.port" placeholder="端口" type="number" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.username" placeholder="用户名">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.password" placeholder="密码" type="password">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<!-- 添加记住密码复选框 -->
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="loginForm.remember">记住密码</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-row justify="end">
|
||||
<el-form-item size="small" :gutter="30">
|
||||
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
||||
<el-button type="primary" @click="onSubmit">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<el-row justify="center">
|
||||
<el-text class="error-message">{{ errorMessage }}</el-text>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<el-row justify="end">
|
||||
<el-text class="plain-text">Powered by TuringAI</el-text>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('./login-bg.jpg');
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('/login-bg.jpg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
min-width: 350px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 10px;
|
||||
background-color: #01111863;
|
||||
min-width: 350px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 10px;
|
||||
background-color: #01111863;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
background-color: #01111863;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
background-color: #01111863;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
background-color: #01111863;
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
color: #c2c8fd;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
background-color: #01111863;
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
}
|
||||
|
||||
.plain-text {
|
||||
color: #9a9db3;
|
||||
color: #9a9db3;
|
||||
}
|
||||
</style>
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-card class="user-list">
|
||||
<div class="header">
|
||||
<el-row class="header-title" :gutter="10">
|
||||
<el-col :span="24">用户管理</el-col>
|
||||
</el-row>
|
||||
<el-row class="header-button" :gutter="10">
|
||||
<el-col :span="2">
|
||||
<div>
|
||||
<el-button type="primary" @click="showAddUserDialog">新增</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<!-- <div>
|
||||
<el-button type="danger" @click="handleDeleteSelected">删除所选</el-button>
|
||||
</div> -->
|
||||
</el-col>
|
||||
<el-col :span="18"></el-col>
|
||||
<el-col :span="2">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="default">表格列设置</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="(column, index) in columns" :key="index">
|
||||
<el-checkbox v-model="column.visible">{{ column.label }}</el-checkbox>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<div class="table-part">
|
||||
<el-table v-loading="loading" :data="tableData"
|
||||
class="user-table" border>
|
||||
<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="序号"
|
||||
min-width="100"></el-table-column>
|
||||
<el-table-column v-if="getColumnVisibility('username')" prop="username" label="用户名"
|
||||
min-width="180"></el-table-column>
|
||||
<el-table-column v-if="getColumnVisibility('email')" prop="email" label="邮箱"
|
||||
min-width="200"></el-table-column>
|
||||
<el-table-column v-if="getColumnVisibility('actions')" label="操作" min-width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="text" size="small"
|
||||
@click="showEditUserDialog(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="small" @click="confirmDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户弹窗 -->
|
||||
<el-dialog :title="isEdit ? '编辑用户' : '新增用户'" v-model="userDialogVisible" width="30%">
|
||||
<el-form :model="newUserForm" ref="newUserFormRef" :rules="rules" label-width="80px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="newUserForm.username" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="newUserForm.email" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="newUserForm.password" type="password" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="userDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" v-if="!isEdit" @click="handleAddUser">新增</el-button>
|
||||
<el-button type="primary" v-else @click="handleEditUser">修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref([
|
||||
{ label: '序号', prop: 'id', visible: true },
|
||||
{ label: '用户名', prop: 'username', visible: true },
|
||||
{ label: '邮箱', prop: 'email', visible: true },
|
||||
{ label: '操作', prop: 'actions', visible: true },
|
||||
{ label: '选择', prop: 'selection', visible: true }
|
||||
]);
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([]); // 动态表格数据
|
||||
const loading = ref(false);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const userDialogVisible = ref(false); // 控制用户弹窗的显示
|
||||
const isEdit = ref(false); // 控制是否为编辑模式
|
||||
|
||||
// 用户表单数据
|
||||
const newUserForm = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
const newUserFormRef = ref(); // 引用表单实例
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
};
|
||||
|
||||
// 从 localStorage 中获取保存的 token
|
||||
const token = localStorage.getItem('alertToken');
|
||||
// const token = "";
|
||||
|
||||
// 创建 BoxApi 实例并设置 token
|
||||
let apiInstance = new BoxApi();
|
||||
if (token) {
|
||||
apiInstance.setToken(token); // 确保在请求前设置 token
|
||||
} else {
|
||||
console.error('Token is missing. Please login.');
|
||||
}
|
||||
|
||||
// 获取列的可见状态
|
||||
const getColumnVisibility = (prop: string) => {
|
||||
const column = columns.value.find((col) => col.prop === prop);
|
||||
return column ? column.visible : false;
|
||||
};
|
||||
|
||||
// 刷新表格数据
|
||||
const refreshTable = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const users = await apiInstance.getAllUsers(); // 调用getAllUsers方法获取用户
|
||||
tableData.value = users.map((user: any, index: number) => ({
|
||||
id: index + 1, // 增加id索引
|
||||
username: user.username,
|
||||
email: user.email
|
||||
}));
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新用户列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 在页面加载时获取用户数据
|
||||
onMounted(() => {
|
||||
refreshTable();
|
||||
});
|
||||
|
||||
// 显示新增用户弹窗
|
||||
const showAddUserDialog = () => {
|
||||
isEdit.value = false; // 切换到新增模式
|
||||
newUserForm.value = { username: '', email: '', password: '' }; // 重置表单
|
||||
userDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 显示编辑用户弹窗
|
||||
const showEditUserDialog = (user: any) => {
|
||||
isEdit.value = true; // 切换到编辑模式
|
||||
newUserForm.value = { ...user, password: '' }; // 填充表单数据,但密码保持为空
|
||||
userDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 新增用户
|
||||
const handleAddUser = () => {
|
||||
newUserFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await apiInstance.addUser(newUserForm.value.username, newUserForm.value.password, newUserForm.value.email);
|
||||
ElMessage.success('新增用户成功');
|
||||
userDialogVisible.value = false;
|
||||
refreshTable();
|
||||
} catch (error: any) {
|
||||
if (error && error.ec !== undefined && error.dm && error.em) {
|
||||
const errMessage = `
|
||||
<div> 错误代码: ${error.ec}</div>
|
||||
<div> 描述: ${error.dm}</div>
|
||||
<div> 信息: ${error.em}</div>
|
||||
`;
|
||||
ElMessage({
|
||||
message: errMessage,
|
||||
type: 'error',
|
||||
dangerouslyUseHTMLString: true,
|
||||
duration: 3000,
|
||||
offset: 50
|
||||
});
|
||||
} else {
|
||||
ElMessage.error('新增用户失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('表单验证失败,请检查输入');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 编辑用户(调用 resetUser 更新所有用户信息)
|
||||
const handleEditUser = () => {
|
||||
newUserFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await apiInstance.resetUser(
|
||||
newUserForm.value.username,
|
||||
newUserForm.value.password,
|
||||
newUserForm.value.email
|
||||
);
|
||||
ElMessage.success('用户信息修改成功');
|
||||
userDialogVisible.value = false;
|
||||
refreshTable();
|
||||
} catch (error: any) {
|
||||
ElMessage.error('修改用户信息失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 确认删除弹窗
|
||||
const confirmDelete = (row: any) => {
|
||||
ElMessageBox.confirm(`是否确认删除用户 ${row.username}?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
handleDelete(row); // 用户确认后执行删除操作
|
||||
}).catch(() => {
|
||||
ElMessage.info('已取消删除');
|
||||
});
|
||||
};
|
||||
|
||||
// 删除单个用户
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await apiInstance.rmUser(row.username); // 调用 rmUser API
|
||||
ElMessage.success(`用户 ${row.username} 删除成功`);
|
||||
refreshTable(); // 删除成功后刷新表格
|
||||
} catch (error) {
|
||||
ElMessage.error('删除用户失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除选中用户
|
||||
const handleDeleteSelected = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的用户');
|
||||
return;
|
||||
}
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.user-list {
|
||||
width: auto;
|
||||
overflow: auto;
|
||||
/* min-height: 650px; */
|
||||
/* height: 100vh; */
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
.user-table{
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.header-button .el-button {
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table-part {
|
||||
overflow: auth;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-left: 1px;
|
||||
/* 外边距设定值后列自动扩大问题 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
26
src/main.ts
26
src/main.ts
|
@ -4,13 +4,37 @@ import router from './router';
|
|||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
// import axiosInstance from '@/utils/axios-config';
|
||||
import axios from 'axios';
|
||||
import '@/assets/global.css'
|
||||
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.provide('axios', axios);
|
||||
// app.provide('axios', axiosInstance);
|
||||
app.use(ElementPlus, { locale: zhCn });
|
||||
app.use (router)
|
||||
|
||||
// 导航守卫,检查登录状态
|
||||
router.beforeEach((to, from, next) => {
|
||||
const token = localStorage.getItem('alertToken'); // 检查 token 是否存在,作为是否登录的依据
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
// 该路由需要认证
|
||||
if (!token) {
|
||||
// 如果没有 token,重定向到登录页面
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath } // 将当前路径传递给登录页面,登录后可以重定向回来
|
||||
});
|
||||
} else {
|
||||
// 已登录,继续访问
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
// 不需要认证,继续访问
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app')
|
||||
|
|
|
@ -1,58 +1,77 @@
|
|||
// 第一步,引入createRouter
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import AlertChart from '@/components/AlertChart.vue';
|
||||
import Statistics from '@/components/Statistics.vue';
|
||||
import AlertManagement from '@/html/AlertManagement.vue';
|
||||
import Test from '@/html/Test.vue';
|
||||
// import Login from '@/html/LoginView.vue';
|
||||
|
||||
import Login from '@/html/LoginView.vue';
|
||||
import UserList from '@/html/UserList.vue';
|
||||
import Home from '@/html/Home.vue';
|
||||
import DataStatistics from '@/html/DataStatistics.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: Layout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/alertManagement',
|
||||
name: 'AlertManagement',
|
||||
component: AlertManagement,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'Test',
|
||||
component: Test,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/alertChart',
|
||||
name: 'AlertChart',
|
||||
component: AlertChart,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
name: 'Statistics',
|
||||
component: Statistics,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path:'/userList',
|
||||
name: 'UserList',
|
||||
component: UserList,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path:'/dataStatistics',
|
||||
name: 'DataStatistics',
|
||||
component: DataStatistics,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: { requiresAuth: false }
|
||||
}
|
||||
// ,
|
||||
// {
|
||||
// path: '/login',
|
||||
// name: 'Login',
|
||||
// component: Login,
|
||||
// meta: { requiresAuth: false }
|
||||
// },
|
||||
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
// axios-config.ts
|
||||
import axios from 'axios';
|
||||
|
||||
// 从 localStorage 中获取地址和端口
|
||||
const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1';
|
||||
const rememberedPort = localStorage.getItem('rememberedPort') || '8000';
|
||||
|
||||
// 动态拼接 baseURL
|
||||
const baseURL = `http://${rememberedAddress}:${rememberedPort}/api/v1`;
|
||||
|
||||
// 创建 axios 实例
|
||||
const axiosInstance = axios.create({
|
||||
// baseURL: 'https://test1.turingvideo.cn/api/v1',
|
||||
baseURL: 'http://127.0.0.1:8000/api/v1',
|
||||
timeout: 10000,
|
||||
baseURL: baseURL, // 使用动态生成的 baseURL
|
||||
timeout: 10000, // 超时时间
|
||||
withCredentials: true, // 使用cookie跨域请求
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
axiosInstance.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('alertToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
@ -20,14 +28,15 @@ axiosInstance.interceptors.request.use(
|
|||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => {
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/#/login'; // 重定向到登录页面
|
||||
// localStorage.removeItem('alertToken');
|
||||
// window.location.href = '/#/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,359 @@
|
|||
import axios from 'axios';
|
||||
|
||||
class BoxApi {
|
||||
|
||||
private readonly defaultPort: number = 8000;
|
||||
|
||||
private readonly apiLogin: string = "/auth/login";
|
||||
private readonly apiLogout: string = "/auth/logout";
|
||||
private readonly apiAdduser: string = "/auth/adduser";
|
||||
private readonly apiRmuser: string = "/auth/rmuser";
|
||||
private readonly apiAllusers: string = "/auth/allusers";
|
||||
private readonly apiCameras: string = "/cameras";
|
||||
private readonly apiEvents: string = "/events";
|
||||
private readonly apiAlgorithms: string = "/algorithms";
|
||||
private readonly apiResetUser: string = "/auth/resetuser";
|
||||
private readonly apiResetPassword: string = "/auth/reset_password";
|
||||
|
||||
private readonly loginConfig: object = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
|
||||
private address: string = "";
|
||||
private port: number = this.defaultPort;
|
||||
private token: string = "";
|
||||
|
||||
private codemap: Map<string, string> = new Map<string, string>();
|
||||
private axios: any = null;
|
||||
|
||||
public constructor(
|
||||
address: string = "",
|
||||
port: number = 0
|
||||
) {
|
||||
// 获取本地存储的address和port
|
||||
const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1';
|
||||
const rememberedPort = localStorage.getItem('rememberedPort') || '8000';
|
||||
|
||||
this.setAddress(address || rememberedAddress);
|
||||
this.setPort(port || parseInt(rememberedPort));
|
||||
|
||||
// 动态创建axios实例,基于当前的address和port
|
||||
this.createAxiosInstance();
|
||||
}
|
||||
|
||||
private createAxiosInstance() {
|
||||
// 动态生成baseURL
|
||||
this.axios = axios.create({
|
||||
baseURL: `http://${this.address}:${this.port}/api/v1`,
|
||||
withCredentials: true
|
||||
});
|
||||
}
|
||||
|
||||
public setAddress(address: string) {
|
||||
this.address = address === "" ? this._boxAddr() : address;
|
||||
this.createAxiosInstance();
|
||||
}
|
||||
|
||||
public setPort(port: number) {
|
||||
this.port = port === 0 ? this.defaultPort : port;
|
||||
this.createAxiosInstance();
|
||||
}
|
||||
|
||||
public setToken(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public async login(username: string, password: string, cookieLess: boolean = false): Promise<any> {
|
||||
const loginData = {
|
||||
username: username,
|
||||
password: password,
|
||||
cookieless: cookieLess ? "True" : "False"
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await this.axios.post(this.apiLogin, loginData, this.loginConfig)
|
||||
console.log(res)
|
||||
if (res.data.err.ec === 0) {
|
||||
this.token = res.data.ret.token;
|
||||
await this.updateCodemap(this.token);
|
||||
return res.data.ret.token;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async logout(token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.post(this.apiLogout, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async resetPassword(newPassword: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const data = {
|
||||
new_password: newPassword
|
||||
};
|
||||
|
||||
const res = await this.axios.post(this.apiResetPassword, data, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async resetUser(username: string, password: string, email: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const data = {
|
||||
username: username,
|
||||
password: password,
|
||||
email: email
|
||||
};
|
||||
|
||||
const res = await this.axios.post(this.apiResetUser, data, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async addUser(username: string, password: string, email: string = "", token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const data = {
|
||||
username: username,
|
||||
password: password,
|
||||
email: email
|
||||
};
|
||||
|
||||
const res = await this.axios.post(this.apiAdduser, data, this._authHeader(token));
|
||||
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw res.data.err;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.data && error.response.data.err) {
|
||||
throw error.response.data.err;
|
||||
} else {
|
||||
throw new Error("网络错误或服务器未响应");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async rmUser(username: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const data = {
|
||||
username: username
|
||||
};
|
||||
|
||||
const res = await this.axios.post(this.apiRmuser, data, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllUsers(token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.get(this.apiAllusers, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret.users;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCamerasByUrl(url: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.get(url, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCameras(limit: number, offset: number, token: string | null = null): Promise<any> {
|
||||
const url = `${this.apiCameras}?limit=${limit}&offset=${offset}`;
|
||||
return await this.getCamerasByUrl(url, token);
|
||||
}
|
||||
|
||||
public async getEventsByUrl(url: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.get(url, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// public async getEvents(limit: number, offset: number, token: string | null = null): Promise<any> {
|
||||
// const url = `${this.apiEvents}?limit=${limit}&offset=${offset}`;
|
||||
// return await this.getEventsByUrl(url, token);
|
||||
// }
|
||||
|
||||
// public async getEvents(token: string | null = null, pageSize: number = 10, currentPage: number = 1): Promise<any> {
|
||||
// const offset = (currentPage - 1) * pageSize;
|
||||
|
||||
// const url = `${this.apiEvents}?limit=${pageSize}&offset=${offset}`;
|
||||
|
||||
// try {
|
||||
// const res = await this.axios.get(url, this._authHeader(token));
|
||||
// if (res.data.err.ec === 0) {
|
||||
// return res.data.ret;
|
||||
// } else {
|
||||
// throw new Error(res.data.err.msg);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
|
||||
public async getEvents(token: string | null = null, pageSize: number = 20, currentPage: number = 1): Promise<any> {
|
||||
// 计算 offset
|
||||
const offset = (currentPage - 1) * pageSize;
|
||||
|
||||
try {
|
||||
// 发送请求,携带 limit 和 offset 参数
|
||||
const res = await this.axios.get(`${this.apiEvents}?limit=${pageSize}&offset=${offset}`, this._authHeader(token));
|
||||
|
||||
// 请求成功,返回数据
|
||||
if (res.data.err.ec === 0) {
|
||||
return {
|
||||
tableData: res.data.ret.results, // 告警数据
|
||||
totalItems: res.data.ret.count // 总条数
|
||||
};
|
||||
} else {
|
||||
// 处理请求失败情况
|
||||
throw new Error(res.data.err);
|
||||
}
|
||||
} catch (error) {
|
||||
// 抛出异常以便前端捕获
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// public async getOneEvent(token: string | null = null): Promise<any> {
|
||||
// try {
|
||||
// return await this.getEvents(1, 0, token);
|
||||
// } catch (error) {
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
public async getOneEvent(token: string | null = null): Promise<any> {
|
||||
try {
|
||||
// 调用 getEvents 方法,设置 pageSize 为 1,currentPage 为 1
|
||||
return await this.getEvents(token, 1, 1);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async setEventStatus(eventId: number, status: string, remark: string | null = null, token: string | null = null): Promise<any> {
|
||||
const url = `${this.apiEvents}/${eventId}`;
|
||||
const newRemark = remark ? remark : "";
|
||||
|
||||
const data = {
|
||||
status: status,
|
||||
remark: newRemark
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await this.axios.patch(url, data, this._authHeader(token))
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAlgorithms(token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.get(this.apiAlgorithms, this._authHeader(token))
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateCodemap(token: string | null = null): Promise<boolean> {
|
||||
try {
|
||||
this.codemap.clear()
|
||||
const algorithms = await this.getAlgorithms(token);
|
||||
algorithms.forEach((algorithm: { code_name: string, name: string }) => {
|
||||
this.codemap.set(algorithm.code_name, algorithm.name)
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public getAlgorithmName(code_name: string): string {
|
||||
return this.codemap.get(code_name) || code_name;
|
||||
}
|
||||
|
||||
private _authHeader(token: string | null = null): object {
|
||||
// const access_token = token === "" ? this.token : token;
|
||||
const alertToken = localStorage.getItem(`alertToken`) || token || this.token || "" ||"";
|
||||
return {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${alertToken}`,
|
||||
// 'Cookie': `jwt=${access_token}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _boxAddr() {
|
||||
return window.location.host.replace(/:\d+/, "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { BoxApi };
|
|
@ -6,7 +6,7 @@ export const getSuperboxApiConfig = (address) => {
|
|||
const LOGIN_ROUTE = BASE_ROUTE + "/auth/login";
|
||||
const LOGOUT_ROUTE = BASE_ROUTE + "/auth/logout";
|
||||
const CAMERA_ROUTE = BASE_ROUTE + "/camera/cameras";
|
||||
const EVENTS_ROUTE = BASE_ROUTE + "/event/events?limit=300";
|
||||
const EVENTS_ROUTE = BASE_ROUTE + "/event/events";
|
||||
const ALGORITHM_ROUTE = BASE_ROUTE + "/algorithms";
|
||||
|
||||
let addr = address;
|
||||
|
@ -86,11 +86,15 @@ export const getCameras = (token, address = null) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getEvents = (token, address = null) => {
|
||||
export const getEvents = (token, address = null, pageSize = 20, currentPage = 1) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getSuperboxApiConfig(address);
|
||||
const offset = (currentPage - 1) * pageSize;
|
||||
const params = new URLSearchParams({ limit: pageSize, offset });
|
||||
const urlWithParams = `${api.events}?${params.toString()}`;
|
||||
// console.log("urlWithParams>>>>>>>>>>>>>>>>>", urlWithParams);
|
||||
axios
|
||||
.get(api.events, {
|
||||
.get(urlWithParams, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
@ -98,7 +102,10 @@ export const getEvents = (token, address = null) => {
|
|||
})
|
||||
.then((res) => {
|
||||
if (res.data.err.ec === 0) {
|
||||
resolve(res.data.ret.results);
|
||||
resolve({
|
||||
tableData: res.data.ret.results,
|
||||
totalItems: res.data.ret.count
|
||||
});
|
||||
}
|
||||
reject(res.data.err);
|
||||
})
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-useless-catch */
|
||||
import axios from 'axios';
|
||||
|
||||
class SuperboxApi {
|
||||
|
@ -7,6 +6,9 @@ class SuperboxApi {
|
|||
|
||||
private readonly apiLogin: string = "/auth/login";
|
||||
private readonly apiLogout: string = "/auth/logout";
|
||||
private readonly apiAdduser: string = "/auth/adduser";
|
||||
private readonly apiRmuser: string = "/auth/rmuser";
|
||||
private readonly apiAllusers: string = "/auth/allusers";
|
||||
private readonly apiCameras: string = "/cameras";
|
||||
private readonly apiEvents: string = "/events";
|
||||
private readonly apiAlgorithms: string = "/algorithms";
|
||||
|
@ -28,7 +30,7 @@ class SuperboxApi {
|
|||
public constructor(
|
||||
address: string = "",
|
||||
port: number = 0
|
||||
) {
|
||||
) {
|
||||
this.setAddress(address);
|
||||
this.setPort(port);
|
||||
this.axios = axios.create({
|
||||
|
@ -53,12 +55,11 @@ class SuperboxApi {
|
|||
const loginData = {
|
||||
username: username,
|
||||
password: password,
|
||||
cookieless: cookieLess ? "True" : "False"
|
||||
cookieless: cookieLess ? "true" : "false" // 这里会根据 cookieLess 的值决定是 "true" 还是 "false"
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const res = await this.axios.post(this.apiLogin, loginData, this.loginConfig)
|
||||
console.log(res)
|
||||
if (res.data.err.ec === 0) {
|
||||
this.token = res.data.ret.token;
|
||||
await this.updateCodemap(this.token);
|
||||
|
@ -84,6 +85,55 @@ class SuperboxApi {
|
|||
}
|
||||
}
|
||||
|
||||
public async addUser(username: string, password: string, email: string="", token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const data = {
|
||||
username: username,
|
||||
password: password,
|
||||
email: email
|
||||
};
|
||||
|
||||
const res = await this.axios.post(this.apiAdduser, data, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async rmUser(username: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const data = {
|
||||
username: username
|
||||
};
|
||||
|
||||
const res = await this.axios.post(this.apiRmuser, data, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllUsers(token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.get(this.apiAllusers, this._authHeader(token));
|
||||
if (res.data.err.ec === 0) {
|
||||
return res.data.ret.users;
|
||||
} else {
|
||||
throw new Error(res.data.err.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCamerasByUrl(url: string, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const res = await this.axios.get(url, this._authHeader(token));
|
||||
|
@ -197,4 +247,4 @@ class SuperboxApi {
|
|||
|
||||
}
|
||||
|
||||
export { SuperboxApi };
|
||||
export { SuperboxApi };
|
||||
|
|
|
@ -15,20 +15,20 @@ export default defineConfig({
|
|||
},
|
||||
server:{
|
||||
// host:'192.168.28.44',
|
||||
host:'127.0.0.1',
|
||||
// host: '192.168.28.32',
|
||||
// host:'192.168.28.44',
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
open:true,
|
||||
cors: true,
|
||||
// proxy: {
|
||||
// '/api': "192.168.28.44:8000"
|
||||
// }
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000', // 目标 API 地址
|
||||
changeOrigin: true, // 开启跨域
|
||||
rewrite: path => path.replace('^/api/', '')
|
||||
}
|
||||
}
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://192.168.28.33:8000',
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace('^/api/', '')
|
||||
// }
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue