Compare commits

...

15 Commits

Author SHA1 Message Date
龚皓 688658d1c2 首页建立-用户列表添加 2024-09-30 15:54:06 +08:00
龚皓 9a6753cfa1 数字动画尝试 2024-09-30 15:53:03 +08:00
龚皓 76dfe72832 登录端口添加 2024-09-30 15:52:11 +08:00
龚皓 31ece3ea1f 侧边栏 2024-09-30 15:51:44 +08:00
龚皓 677804fbf6 告警列表 2024-09-30 15:48:48 +08:00
龚皓 8acedde40f 图标组件化页面 2024-09-30 15:46:37 +08:00
龚皓 d423ea00c0 路由设置 2024-09-30 15:45:00 +08:00
龚皓 c76d2e498c 合并superbox等控制接口 2024-09-30 15:44:17 +08:00
龚皓 376981cf5e 路由守卫 2024-09-30 15:43:01 +08:00
龚皓 d580f303d0 dayjs-config 2024-09-30 15:39:50 +08:00
龚皓 e307cd7e94 底边栏设置 2024-09-06 09:15:32 +08:00
龚皓 12676a3706 基本布局 2024-09-05 16:32:46 +08:00
龚皓 631be0cbbd 全局引用 2024-09-05 16:32:06 +08:00
龚皓 b5f8e0e89f 全局静态资源设置 2024-09-05 16:31:23 +08:00
龚皓 b5044b700f 图片 2024-09-05 16:30:43 +08:00
19 changed files with 2143 additions and 646 deletions

7
package-lock.json generated
View File

@ -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",

View File

@ -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",

BIN
public/turing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

5
src/assets/global.css Normal file
View File

@ -0,0 +1,5 @@
body{
margin: 0;
padding: 0;
}

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

317
src/html/DataStatistics.vue Normal file
View File

@ -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>

7
src/html/Home.vue Normal file
View File

@ -0,0 +1,7 @@
<template>
<div>
<h1>
首页
</h1>
</div>
</template>

View File

@ -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>

308
src/html/UserList.vue Normal file
View File

@ -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>

View File

@ -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')

View File

@ -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;

View File

@ -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);
}

359
src/utils/boxApi.ts Normal file
View File

@ -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 为 1currentPage 为 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 };

View File

@ -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);
})

View File

@ -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 };

View File

@ -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/', '')
// }
// }
}
})