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",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"element-plus": "^2.7.8",
|
"element-plus": "^2.7.8",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.4.29",
|
||||||
|
@ -1074,9 +1075,9 @@
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.12",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.12.tgz",
|
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
"integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||||
},
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"element-plus": "^2.7.8",
|
"element-plus": "^2.7.8",
|
||||||
"vue": "^3.4.29",
|
"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">
|
<el-card class="alert-card">
|
||||||
<div class="alert-header">告警趋势</div>
|
<div class="alert-header">告警趋势</div>
|
||||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||||
<el-tab-pane label="前一天" name="first">前一天数据</el-tab-pane>
|
<el-tab-pane label="今日" name="first">
|
||||||
<el-tab-pane label="3天" name="second">近3天数据展示</el-tab-pane>
|
<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>
|
</el-tabs>
|
||||||
<div id="alertChart" class="chart-container"></div>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
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 activeName = ref('first');
|
||||||
const password = '1234qwer!';
|
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 {
|
const apiInstance = new BoxApi(); // 创建 BoxApi 实例
|
||||||
name: 'AlertChart',
|
|
||||||
data() {
|
// 计算前7天的日期
|
||||||
return {
|
const calculateWeekDates = () => {
|
||||||
activeName: 'first',
|
const today = dayjs();
|
||||||
eventsData: []
|
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') {
|
chartInstanceWeek.value.setOption(option);
|
||||||
option = this.get24HourChartOption();
|
}
|
||||||
} else if (this.activeName === 'second') {
|
};
|
||||||
option = this.get3DayChartOption();
|
|
||||||
|
// 初始化今日图表
|
||||||
|
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);
|
// 统计最近7天的告警数量
|
||||||
},
|
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||||
get24HourChartOption() {
|
weekDates.value.forEach((date, index) => {
|
||||||
const now = new Date();
|
if (endedDay === date) {
|
||||||
const todayMidnight = new Date(now);
|
dailyCounts.value[index] += 1;
|
||||||
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]++;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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>
|
</script>
|
||||||
|
@ -194,29 +303,28 @@ export default {
|
||||||
.alert-card {
|
.alert-card {
|
||||||
background-color: #2a3f54;
|
background-color: #2a3f54;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-header {
|
.alert-header {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #3a4b5c;
|
border-bottom: 1px solid #3a4b5c;
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-tabs {
|
.alert-tabs {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-tabs__item {
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 780px;
|
||||||
height: 400px;
|
height: 420px;
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs__item {
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__active-bar {
|
.el-tabs__active-bar {
|
||||||
|
|
|
@ -1,34 +1,161 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="layout" class="layout-container">
|
<div id="layout" class="layout-container">
|
||||||
<el-aside width="200px" class="nav-sidebar">
|
<el-aside class="nav-sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<el-image src="/logo.png" fit="contain" />
|
<el-image src="/turing.png" fit="contain" />
|
||||||
</div>
|
</div>
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-part" router>
|
<!-- <el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
|
||||||
<el-menu-item index="/">告警管理</el-menu-item>
|
<el-radio-button :value="false">expand</el-radio-button>
|
||||||
<el-menu-item index="/test">测试</el-menu-item>
|
<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-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
<div class="content-layout">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<el-header class="nav-header">
|
||||||
|
|
||||||
<el-main class="main-content">
|
<div class="header-right">
|
||||||
<router-view />
|
<!-- 用户头像下拉菜单 -->
|
||||||
</el-main>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
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 route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const activeIndex = ref(route.path);
|
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(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
(newPath) => {
|
(newPath) => {
|
||||||
activeIndex.value = 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -37,14 +164,18 @@ watch(
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
/* padding: 1px; */
|
padding: 1px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-sidebar {
|
.nav-sidebar {
|
||||||
background-color: #1f2d3d;
|
width: 200px;
|
||||||
|
background-color: #001529;
|
||||||
/* width: 200px; */
|
/* width: 200px; */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
min-height: 100vh;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
@ -68,14 +199,86 @@ watch(
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
background-color: #001529;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 悬停样式 */
|
||||||
.el-menu-part .el-menu-item:hover {
|
.el-menu-part .el-menu-item:hover {
|
||||||
|
background-color: #001529;
|
||||||
color: #00aaff;
|
color: #00aaff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
/* 选中样式 */
|
||||||
flex-grow: 1;
|
.el-menu-part .el-menu-item.is-active {
|
||||||
padding: 20px;
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
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',
|
const fetchTypeMapping = async (token) => {
|
||||||
data() {
|
const algorithms = await apiInstance.getAlgorithms(token);
|
||||||
return {
|
let mapping = algorithms.map(algorithm => ({
|
||||||
chart: null,
|
value: 0,
|
||||||
seriesData: [
|
code_name: algorithm.code_name,
|
||||||
|
name: algorithm.name
|
||||||
|
}));
|
||||||
|
|
||||||
],
|
// 添加额外的类型
|
||||||
};
|
const newMapping = [
|
||||||
},
|
{ code_name: "minizawu:532", name: "杂物堆积", value: 0 }
|
||||||
// mounted() {
|
];
|
||||||
// this.initChart();
|
|
||||||
// },
|
seriesData.value = mapping.concat(newMapping);
|
||||||
async created(){
|
};
|
||||||
try{
|
|
||||||
const token = await login(username, password);
|
// 分批次获取全量告警数据并更新 seriesData
|
||||||
await this.fetchTypeMapping(token);
|
const fetchAndProcessEvents = async (token) => {
|
||||||
await this.updateSeriesData(token);
|
try {
|
||||||
this.initChart();
|
let currentPage = 1;
|
||||||
}catch(error){
|
const pageSize = 2000; // 每次加载 2000 条
|
||||||
console.error("Error fetching algorithms:", error);
|
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;
|
processEventData(allEvents);
|
||||||
},
|
updateChart(); // 最终更新图表
|
||||||
async updateSeriesData(token){
|
} catch (error) {
|
||||||
const events = await getEvents(token);
|
console.error("Error fetching events:", error);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理告警事件数据并更新 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stats-card {
|
.stats-card {
|
||||||
background-color: #2a3f54;
|
background-color: #2a3f54;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-header {
|
.stats-header {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #3a4b5c;
|
border-bottom: 1px solid #3a4b5c;
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-row {
|
.stats-row {
|
||||||
/* margin-top: 20px; */
|
margin-top: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 500px;
|
height: 445px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,28 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="home">
|
<div class="alert-container">
|
||||||
<el-row>
|
<el-row class="top-pan">
|
||||||
<el-col :span="16">
|
<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">
|
<div class="table-container">
|
||||||
<el-table :data="paginatedData">
|
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header"
|
||||||
<el-table-column prop="id" label="告警编号" width="140"></el-table-column>
|
:fit="true">
|
||||||
<el-table-column label="告警类型" width="140">
|
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
||||||
|
<el-table-column label="告警类型" min-width="150">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<!-- {{ typeMapping[scope.row.types] || codenameTranslate(scope.row.types) }} -->
|
|
||||||
{{ typeMapping[scope.row.types] }}
|
{{ typeMapping[scope.row.types] }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="camera.name" label="告警位置" width="180"></el-table-column>
|
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
||||||
<el-table-column label="告警时间" width="180">
|
<el-table-column label="告警时间" min-width="200">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
{{ formatDateTime(scope.row.started_at) }}
|
{{ formatDateTime(scope.row.ended_at) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="告警状态" width="140">
|
<el-table-column prop="status" label="告警状态" min-width="100">
|
||||||
<template v-slot="scope">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="140">
|
<el-table-column label="操作" min-width="100">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,23 +44,11 @@
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="tableData.length"
|
<el-pagination @size-change="handleSizeChange" @current-change="handlePageChange" :current-page="currentPage"
|
||||||
:page-size="pageSize" :current-page.sync="currentPage" @current-change="handlePageChange"
|
:page-size="pageSize" :total="totalItems" layout="total, sizes, prev, pager, next, jumper">
|
||||||
@size-change="handleSizeChange">
|
|
||||||
</el-pagination>
|
</el-pagination>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</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-row>
|
||||||
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
||||||
<div>
|
<div>
|
||||||
|
@ -55,7 +57,6 @@
|
||||||
<p>告警编号: {{ selectedRow.id }}</p>
|
<p>告警编号: {{ selectedRow.id }}</p>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<!-- <p>告警类型: {{ typeMapping[selectedRow.types] || codenameTranslate(selectedRow.types) }}</p> -->
|
|
||||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
|
@ -67,7 +68,8 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="6">
|
<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-tag></p>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
|
@ -100,222 +102,180 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import * as echarts from 'echarts';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import Statistics from '@/components/Statistics.vue';
|
import Statistics from '@/components/Statistics.vue';
|
||||||
import AlertChart from '@/components/AlertChart.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';
|
// 创建 BoxApi 实例
|
||||||
const password = '1234qwer!';
|
const boxApi = new BoxApi();
|
||||||
|
|
||||||
export default {
|
// 响应式数据
|
||||||
name: 'AlertManagement',
|
const tableData = ref([]);
|
||||||
components: {
|
const dialogVisible = ref(false);
|
||||||
Statistics,
|
const selectedRow = ref({});
|
||||||
AlertChart,
|
const mediums = ref([]);
|
||||||
},
|
const duration = ref('');
|
||||||
data() {
|
const typeMapping = reactive({});
|
||||||
return {
|
const statusMapping = {
|
||||||
tableData: [],
|
'pending': '待处理',
|
||||||
dialogVisible: false,
|
'closed': '已处理'
|
||||||
selectedRow: {},
|
};
|
||||||
mediums: {},
|
const currentPage = ref(1);
|
||||||
duration: '',
|
const pageSize = ref(20);
|
||||||
typeMapping: {
|
const totalItems = ref(0);
|
||||||
// 'abnormal:525': '黑屏检测',
|
const token = ref(null);
|
||||||
// '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 fetchTypeMapping = async (token) => {
|
||||||
async created() {
|
try {
|
||||||
try {
|
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||||
const token = await login(username, password);
|
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||||
// await initCodeNameMap(token);
|
algorithms.forEach((algorithm) => {
|
||||||
this.tableData = await getEvents(token);
|
typeMapping[algorithm.code_name] = algorithm.name;
|
||||||
this.typeMapping = await this.fetchTypeMapping(token);
|
});
|
||||||
console.log(this.tableData);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error fetching algorithms:", 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 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.home {
|
.alert-container {
|
||||||
padding: 40px;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb {
|
.top-pan {
|
||||||
margin-bottom: 30px;
|
padding: 0px;
|
||||||
}
|
margin: 0px;
|
||||||
|
|
||||||
.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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 10px;
|
||||||
gap: 20px;
|
background-color: #fff;
|
||||||
padding: 10px 10px 400px 10px;
|
overflow: auto;
|
||||||
background-color: #f5f5f5;
|
|
||||||
height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-section {
|
.panel-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
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;
|
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 {
|
.event-media {
|
||||||
/* display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px; */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
/* 添加这个属性来左右等分 */
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-container {
|
.media-container {
|
||||||
flex: 0 0 48%;
|
flex: 0 0 48%;
|
||||||
/* 确保每块区域占据一行的48% */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* 使内容在区域内居中 */
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
/* 添加底部间距,使多个元素之间有间隔 */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 确保padding和border不会影响大小 */
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
/* 添加内边距,使内容不靠近边框 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-item video,
|
.video-item video,
|
||||||
.image-item img {
|
.image-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* 保证视频和图片在容器内等宽 */
|
|
||||||
height: auto;
|
height: auto;
|
||||||
/* 保证视频和图片比例正常 */
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
/* 圆润效果 */
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
/* 阴影效果 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-item p,
|
.video-item p,
|
||||||
|
@ -328,7 +288,12 @@ export default {
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 20px;
|
background-color: #e8e9e4;
|
||||||
margin-right: 150px;
|
}
|
||||||
|
|
||||||
|
::v-deep .pagination-container .el-pagination__total,
|
||||||
|
::v-deep .pagination-container .el-pagination__goto,
|
||||||
|
::v-deep .pagination-container .el-pagination__classifier {
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<script lang="ts" setup>
|
||||||
import { reactive } from 'vue'
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import {ElMessage} from 'element-plus'
|
import { BoxApi } from '@/utils/boxApi';
|
||||||
import { Lock, User } from '@element-plus/icons-vue';
|
import { User, Lock } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { useSuperbox } from '@/utils/Superbox'
|
|
||||||
|
|
||||||
import { useSetToken } from '@/utils/misc';
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const superbox = useSuperbox();
|
const route = useRoute();
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
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 () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const token = await superbox.login(loginForm.username, loginForm.password, false);
|
// 检查服务器地址和端口是否填写
|
||||||
if (token) {
|
if (!loginForm.address || !loginForm.port) {
|
||||||
setToken(token);
|
errorMessage.value = '请输入服务器地址和端口';
|
||||||
router.push('/');
|
return;
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage(
|
|
||||||
{
|
|
||||||
message: '用户名或密码错误,登录失败',
|
|
||||||
type: 'error',
|
|
||||||
duration: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实例化 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 = () => {
|
const onReset = () => {
|
||||||
loginForm.username = '';
|
loginForm.username = '';
|
||||||
loginForm.password = '';
|
loginForm.password = '';
|
||||||
|
loginForm.address = '';
|
||||||
|
loginForm.port = '';
|
||||||
|
loginForm.remember = false; // 重置记住密码复选框
|
||||||
|
errorMessage.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
|
<div class="login-form">
|
||||||
<div class="login-form">
|
<div class="login-header">
|
||||||
<div class="login-header">
|
<el-row justify="center">
|
||||||
<el-row justify="center">
|
<el-image src="/logo.png" fit="contain" />
|
||||||
<el-image src="./logo.png" fit="contain" />
|
</el-row>
|
||||||
</el-row>
|
</div>
|
||||||
</div>
|
<div class="login-body">
|
||||||
<div class="login-body">
|
<el-form :model="loginForm" style="max-width: 600px;">
|
||||||
<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.username" placeholder="用户名" clearable>
|
<el-form-item>
|
||||||
<template #prefix>
|
<el-input v-model="loginForm.port" placeholder="端口" type="number" />
|
||||||
<el-icon class="el-input__icon">
|
</el-form-item>
|
||||||
<User />
|
<el-form-item>
|
||||||
</el-icon>
|
<el-input v-model="loginForm.username" placeholder="用户名">
|
||||||
</template>
|
<template #prefix>
|
||||||
</el-input>
|
<el-icon class="el-input__icon">
|
||||||
</el-form-item>
|
<User />
|
||||||
|
</el-icon>
|
||||||
<el-form-item>
|
</template>
|
||||||
<el-input v-model="loginForm.password" placeholder="密码" type="password" show-password clearable>
|
</el-input>
|
||||||
<template #prefix>
|
</el-form-item>
|
||||||
<el-icon class="el-input__icon">
|
<el-form-item>
|
||||||
<Lock />
|
<el-input v-model="loginForm.password" placeholder="密码" type="password">
|
||||||
</el-icon>
|
<template #prefix>
|
||||||
</template>
|
<el-icon class="el-input__icon">
|
||||||
</el-input>
|
<Lock />
|
||||||
</el-form-item>
|
</el-icon>
|
||||||
<el-row justify="end">
|
</template>
|
||||||
<el-form-item size="small" :gutter="30">
|
</el-input>
|
||||||
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="onSubmit">登录</el-button>
|
<!-- 添加记住密码复选框 -->
|
||||||
</el-form-item>
|
<el-form-item>
|
||||||
</el-row>
|
<el-checkbox v-model="loginForm.remember">记住密码</el-checkbox>
|
||||||
</el-form>
|
</el-form-item>
|
||||||
</div>
|
<el-row justify="end">
|
||||||
|
<el-form-item size="small" :gutter="30">
|
||||||
<div class="login-footer">
|
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
||||||
<el-row justify="end">
|
<el-button type="primary" @click="onSubmit">登录</el-button>
|
||||||
<el-text class="plain-text">Powered by TuringAI</el-text>
|
</el-form-item>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
<el-row justify="center">
|
||||||
|
<el-text class="error-message">{{ errorMessage }}</el-text>
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main-layout {
|
.main-layout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-image: url('./login-bg.jpg');
|
background-image: url('/login-bg.jpg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #01111863;
|
background-color: #01111863;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-body {
|
.login-body {
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
background-color: #01111863;
|
background-color: #01111863;
|
||||||
border-radius: 10px 10px 0px 0px;
|
border-radius: 10px 10px 0px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-footer {
|
.login-footer {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
background-color: #01111863;
|
background-color: #01111863;
|
||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.main-title {
|
|
||||||
color: #c2c8fd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plain-text {
|
.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 ElementPlus from 'element-plus';
|
||||||
import 'element-plus/dist/index.css';
|
import 'element-plus/dist/index.css';
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||||
|
// import axiosInstance from '@/utils/axios-config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import '@/assets/global.css'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.provide('axios', axios);
|
// app.provide('axios', axiosInstance);
|
||||||
app.use(ElementPlus, { locale: zhCn });
|
app.use(ElementPlus, { locale: zhCn });
|
||||||
app.use (router)
|
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')
|
app.mount('#app')
|
||||||
|
|
|
@ -1,58 +1,77 @@
|
||||||
// 第一步,引入createRouter
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
|
||||||
|
|
||||||
import Layout from '@/components/Layout.vue';
|
import Layout from '@/components/Layout.vue';
|
||||||
import AlertChart from '@/components/AlertChart.vue';
|
import AlertChart from '@/components/AlertChart.vue';
|
||||||
import Statistics from '@/components/Statistics.vue';
|
import Statistics from '@/components/Statistics.vue';
|
||||||
import AlertManagement from '@/html/AlertManagement.vue';
|
import AlertManagement from '@/html/AlertManagement.vue';
|
||||||
import Test from '@/html/Test.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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/alertManagement',
|
||||||
name: 'AlertManagement',
|
name: 'AlertManagement',
|
||||||
component: AlertManagement,
|
component: AlertManagement,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/test',
|
path: '/test',
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
component: Test,
|
component: Test,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alertChart',
|
path: '/alertChart',
|
||||||
name: 'AlertChart',
|
name: 'AlertChart',
|
||||||
component: AlertChart,
|
component: AlertChart,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/statistics',
|
path: '/statistics',
|
||||||
name: 'Statistics',
|
name: 'Statistics',
|
||||||
component: 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({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes
|
routes
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
export default router;
|
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
// axios-config.ts
|
|
||||||
import axios from 'axios';
|
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({
|
const axiosInstance = axios.create({
|
||||||
// baseURL: 'https://test1.turingvideo.cn/api/v1',
|
baseURL: baseURL, // 使用动态生成的 baseURL
|
||||||
baseURL: 'http://127.0.0.1:8000/api/v1',
|
timeout: 10000, // 超时时间
|
||||||
timeout: 10000,
|
withCredentials: true, // 使用cookie跨域请求
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
axiosInstance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('alertToken');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
@ -20,14 +28,15 @@ axiosInstance.interceptors.request.use(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
axiosInstance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
response => {
|
response => {
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
localStorage.removeItem('token');
|
// localStorage.removeItem('alertToken');
|
||||||
window.location.href = '/#/login'; // 重定向到登录页面
|
// window.location.href = '/#/login';
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
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 LOGIN_ROUTE = BASE_ROUTE + "/auth/login";
|
||||||
const LOGOUT_ROUTE = BASE_ROUTE + "/auth/logout";
|
const LOGOUT_ROUTE = BASE_ROUTE + "/auth/logout";
|
||||||
const CAMERA_ROUTE = BASE_ROUTE + "/camera/cameras";
|
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";
|
const ALGORITHM_ROUTE = BASE_ROUTE + "/algorithms";
|
||||||
|
|
||||||
let addr = address;
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const api = getSuperboxApiConfig(address);
|
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
|
axios
|
||||||
.get(api.events, {
|
.get(urlWithParams, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
|
@ -98,7 +102,10 @@ export const getEvents = (token, address = null) => {
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data.err.ec === 0) {
|
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);
|
reject(res.data.err);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-useless-catch */
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
class SuperboxApi {
|
class SuperboxApi {
|
||||||
|
@ -7,6 +6,9 @@ class SuperboxApi {
|
||||||
|
|
||||||
private readonly apiLogin: string = "/auth/login";
|
private readonly apiLogin: string = "/auth/login";
|
||||||
private readonly apiLogout: string = "/auth/logout";
|
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 apiCameras: string = "/cameras";
|
||||||
private readonly apiEvents: string = "/events";
|
private readonly apiEvents: string = "/events";
|
||||||
private readonly apiAlgorithms: string = "/algorithms";
|
private readonly apiAlgorithms: string = "/algorithms";
|
||||||
|
@ -28,7 +30,7 @@ class SuperboxApi {
|
||||||
public constructor(
|
public constructor(
|
||||||
address: string = "",
|
address: string = "",
|
||||||
port: number = 0
|
port: number = 0
|
||||||
) {
|
) {
|
||||||
this.setAddress(address);
|
this.setAddress(address);
|
||||||
this.setPort(port);
|
this.setPort(port);
|
||||||
this.axios = axios.create({
|
this.axios = axios.create({
|
||||||
|
@ -53,12 +55,11 @@ class SuperboxApi {
|
||||||
const loginData = {
|
const loginData = {
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
cookieless: cookieLess ? "True" : "False"
|
cookieless: cookieLess ? "true" : "false" // 这里会根据 cookieLess 的值决定是 "true" 还是 "false"
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.axios.post(this.apiLogin, loginData, this.loginConfig)
|
const res = await this.axios.post(this.apiLogin, loginData, this.loginConfig)
|
||||||
console.log(res)
|
|
||||||
if (res.data.err.ec === 0) {
|
if (res.data.err.ec === 0) {
|
||||||
this.token = res.data.ret.token;
|
this.token = res.data.ret.token;
|
||||||
await this.updateCodemap(this.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> {
|
public async getCamerasByUrl(url: string, token: string | null = null): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const res = await this.axios.get(url, this._authHeader(token));
|
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:{
|
server:{
|
||||||
// host:'192.168.28.44',
|
// host:'192.168.28.44',
|
||||||
host:'127.0.0.1',
|
// host:'192.168.28.44',
|
||||||
// host: '192.168.28.32',
|
host: '127.0.0.1',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
open:true,
|
open:true,
|
||||||
cors: true,
|
cors: true,
|
||||||
// proxy: {
|
// proxy: {
|
||||||
// '/api': "192.168.28.44:8000"
|
// '/api': "192.168.28.44:8000"
|
||||||
// }
|
// }
|
||||||
proxy: {
|
// proxy: {
|
||||||
'/api': {
|
// '/api': {
|
||||||
target: 'http://127.0.0.1:8000', // 目标 API 地址
|
// target: 'http://192.168.28.33:8000',
|
||||||
changeOrigin: true, // 开启跨域
|
// changeOrigin: true,
|
||||||
rewrite: path => path.replace('^/api/', '')
|
// rewrite: path => path.replace('^/api/', '')
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue