本地告警页面V0
This commit is contained in:
commit
c9ba2ad556
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
# alarm_managerment
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 依赖包
|
||||||
|
|
||||||
|
```
|
||||||
|
"axios": "^1.7.3",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
|
"element-plus": "^2.7.8",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"http-proxy-middleware": "^3.0.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"qs": "^6.12.3",
|
||||||
|
"vue": "^3.4.29",
|
||||||
|
"vue-router": "^4.4.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
> - axios:异步
|
||||||
|
> - echarts:图标盘
|
||||||
|
> - element-plus: 组件
|
||||||
|
> - file-saver: 文件加载
|
||||||
|
> - http-proxy-middleware: 请求跨域
|
||||||
|
> - jszip:下载打包
|
||||||
|
> - qs:静态资源请求
|
||||||
|
> - vue:框架
|
||||||
|
> - vue-router:路由
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
卷 文档 的文件夹 PATH 列表
|
||||||
|
卷序列号为 8BA4-D80A
|
||||||
|
D:\JAVA\DEMO\TURING\ALARM\ALARM_DEV\ALARM_MANAGERMENT
|
||||||
|
│ .gitignore
|
||||||
|
│ dist.zip
|
||||||
|
│ env.d.ts
|
||||||
|
│ index.html
|
||||||
|
│ package-lock.json
|
||||||
|
│ package.json
|
||||||
|
│ README.md
|
||||||
|
│ tree.txt
|
||||||
|
│ tsconfig.app.json
|
||||||
|
│ tsconfig.json
|
||||||
|
│ tsconfig.node.json
|
||||||
|
│ vite.config.ts
|
||||||
|
│ vite.config.ts.timestamp-1722563872625-dafa6df9d3ee1.mjs
|
||||||
|
│
|
||||||
|
├─.vscode
|
||||||
|
│ extensions.json
|
||||||
|
│
|
||||||
|
├─dist
|
||||||
|
│ │ icon.ico
|
||||||
|
│ │ index.html
|
||||||
|
│ │ login-bg.jpg
|
||||||
|
│ │ logo.png
|
||||||
|
│ │
|
||||||
|
│ └─assets
|
||||||
|
│ index-ChTHFsuW.css
|
||||||
|
│ index-DvIh3OSh.js
|
||||||
|
│
|
||||||
|
├─node_modules
|
||||||
|
│
|
||||||
|
├─public
|
||||||
|
│ icon.ico
|
||||||
|
│ login-bg.jpg
|
||||||
|
│ logo.png
|
||||||
|
│
|
||||||
|
└─src
|
||||||
|
│ App.vue
|
||||||
|
│ main.ts
|
||||||
|
│
|
||||||
|
├─components
|
||||||
|
│ DemoPart.vue
|
||||||
|
│ Layout.vue
|
||||||
|
│
|
||||||
|
├─html
|
||||||
|
│ DeepFlow.vue
|
||||||
|
│ DemoToHK.vue
|
||||||
|
│ Flow.vue
|
||||||
|
│ Login.vue
|
||||||
|
│ PassFlow.vue
|
||||||
|
│ Point.vue
|
||||||
|
│
|
||||||
|
├─router
|
||||||
|
│ index.ts
|
||||||
|
│
|
||||||
|
├─static
|
||||||
|
│ login-bg.jpg
|
||||||
|
│ logo.png
|
||||||
|
│
|
||||||
|
└─utils
|
||||||
|
axios-config.ts
|
||||||
|
misc.ts
|
||||||
|
store.ts
|
||||||
|
Superbox.ts
|
||||||
|
type.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Src文件说明
|
||||||
|
|
||||||
|
### 目录分解
|
||||||
|
|
||||||
|
```
|
||||||
|
└─src
|
||||||
|
│ App.vue
|
||||||
|
│ main.ts
|
||||||
|
│
|
||||||
|
├─components
|
||||||
|
│ DemoPart.vue
|
||||||
|
│ Layout.vue
|
||||||
|
│
|
||||||
|
├─html
|
||||||
|
│ DeepFlow.vue
|
||||||
|
│ DemoToHK.vue
|
||||||
|
│ Flow.vue
|
||||||
|
│ Login.vue
|
||||||
|
│ PassFlow.vue
|
||||||
|
│ Point.vue
|
||||||
|
│
|
||||||
|
├─router
|
||||||
|
│ index.ts
|
||||||
|
│
|
||||||
|
├─static
|
||||||
|
│ login-bg.jpg
|
||||||
|
│ logo.png
|
||||||
|
│
|
||||||
|
└─utils
|
||||||
|
axios-config.ts
|
||||||
|
misc.ts
|
||||||
|
store.ts
|
||||||
|
Superbox.ts
|
||||||
|
type.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 根目录
|
||||||
|
|
||||||
|
```
|
||||||
|
└─src
|
||||||
|
│ App.vue
|
||||||
|
│ main.ts
|
||||||
|
├─components
|
||||||
|
├─html
|
||||||
|
├─router
|
||||||
|
├─static
|
||||||
|
└─utils
|
||||||
|
```
|
||||||
|
|
||||||
|
#### App.vue(关键)
|
||||||
|
|
||||||
|
- 在index.html中可见
|
||||||
|
|
||||||
|
```
|
||||||
|
<div id="app"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
> 页面的app为页面主体框架结构,所有组件仿照类似嵌套方式申明和使用,其下囊括页面布局的其他页面和子页面组件
|
||||||
|
> 相当于整体布局的一个载体(根父组件)
|
||||||
|
|
||||||
|
```
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { provide } from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
// setup() {
|
||||||
|
//定义响应式数据
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 添加你的样式 */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
> 作为父页面,不可避免需要向其下子组件传递参数,可通过导入vue中的provide方法直接传递多层次的子组件信息,子组件inject获取信息,当然在vue3中数据的变化需要用reactive包裹数据,创建响应式变化。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### main.ts(关键)
|
||||||
|
|
||||||
|
> 对于app组件的挂载和改改父组件的依赖使用和申明需要在这里定义使用,当一些新的外部组件引用逻辑正确但是显示异常,可检查是否在根文件中引用
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 路由(Route)
|
||||||
|
|
||||||
|
```
|
||||||
|
└─src
|
||||||
|
├─router
|
||||||
|
│ index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
- 看门狗(to,form,next)
|
||||||
|
|
||||||
|
```
|
||||||
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: '',
|
||||||
|
component: ,
|
||||||
|
children: []
|
||||||
|
|
||||||
|
}
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
});
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (to.matched.some(record => record.meta.requiresAuth) && !token) {
|
||||||
|
next({ name: 'Login' });
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export default router;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 组件页面
|
||||||
|
|
||||||
|
#### Layout.vue
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/icon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>告警管理</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "local_alert",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.3",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
|
"element-plus": "^2.7.8",
|
||||||
|
"vue": "^3.4.29",
|
||||||
|
"vue-router": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node20": "^20.1.4",
|
||||||
|
"@types/node": "^20.14.5",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"npm-run-all2": "^6.2.0",
|
||||||
|
"typescript": "~5.4.0",
|
||||||
|
"vite": "^5.3.1",
|
||||||
|
"vue-tsc": "^2.0.21"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 335 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { provide } from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 添加你的样式 */
|
||||||
|
</style>
|
|
@ -0,0 +1,225 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card class="alert-card">
|
||||||
|
<div class="alert-header">告警趋势</div>
|
||||||
|
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||||
|
<el-tab-pane label="前一天" name="first">前一天数据</el-tab-pane>
|
||||||
|
<el-tab-pane label="3天" name="second">近3天数据展示</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<div id="alertChart" class="chart-container"></div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { login, getEvents } from '@/utils/superbox.js';
|
||||||
|
|
||||||
|
const username = 'turingvideo';
|
||||||
|
const password = '1234qwer!';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AlertChart',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeName: 'first',
|
||||||
|
eventsData: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
const token = await login(username, password);
|
||||||
|
this.eventsData = await getEvents(token);
|
||||||
|
this.initChart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching events:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick(tab, event) {
|
||||||
|
this.initChart();
|
||||||
|
},
|
||||||
|
initChart() {
|
||||||
|
var chartDom = document.getElementById('alertChart');
|
||||||
|
var myChart = echarts.init(chartDom);
|
||||||
|
var option;
|
||||||
|
|
||||||
|
if (this.activeName === 'first') {
|
||||||
|
option = this.get24HourChartOption();
|
||||||
|
} else if (this.activeName === 'second') {
|
||||||
|
option = this.get3DayChartOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
option && myChart.setOption(option);
|
||||||
|
},
|
||||||
|
get24HourChartOption() {
|
||||||
|
const now = new Date();
|
||||||
|
const todayMidnight = new Date(now);
|
||||||
|
todayMidnight.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const yesterdayMidnight = new Date(todayMidnight);
|
||||||
|
yesterdayMidnight.setDate(todayMidnight.getDate() - 1);
|
||||||
|
|
||||||
|
const timePoints = ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'];
|
||||||
|
const alertCounts = Array(timePoints.length).fill(0);
|
||||||
|
|
||||||
|
this.eventsData.forEach(event => {
|
||||||
|
const eventTime = new Date(event.started_at);
|
||||||
|
if (eventTime >= yesterdayMidnight && eventTime < todayMidnight) {
|
||||||
|
const hours = eventTime.getHours();
|
||||||
|
if (hours < 4) alertCounts[1]++;
|
||||||
|
else if (hours < 8) alertCounts[2]++;
|
||||||
|
else if (hours < 12) alertCounts[3]++;
|
||||||
|
else if (hours < 16) alertCounts[4]++;
|
||||||
|
else if (hours < 20) alertCounts[5]++;
|
||||||
|
else if (hours < 24) alertCounts[6]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.alert-card {
|
||||||
|
background-color: #2a3f54;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #3a4b5c;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-tabs {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__active-bar {
|
||||||
|
background-color: #409EFF;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,167 @@
|
||||||
|
<template>
|
||||||
|
<div id="layout">
|
||||||
|
<el-header >
|
||||||
|
<div class="logo">
|
||||||
|
<span>管理平台</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-menu">
|
||||||
|
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" router>
|
||||||
|
<el-menu-item index="/" style="margin-left:230px">告警管理</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span>客流分析</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span @click="showDialog = true" style="cursor: pointer;">选择摊位</span>
|
||||||
|
<span v-if="selectedStoreName">| 已选择: {{ selectedStoreName }}</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>区域管理</span>
|
||||||
|
<el-dropdown @command="handleCommand" placement="bottom-end">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
{{ username }} <i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="logout">登出</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
// import type { Store } from '@/utils/type'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Layout',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const token = ref(localStorage.getItem('token') || '');
|
||||||
|
const isUserLoggedIn = computed(() => !!token.value);
|
||||||
|
const username = ref(localStorage.getItem('username') || '');
|
||||||
|
// const isUserLoggedIn = computed(() => !!username.value);
|
||||||
|
const activeIndex = ref(route.path);
|
||||||
|
const showDialog = ref(false);
|
||||||
|
// const selectedStore = ref<Store | null>(null);
|
||||||
|
const selectedStoreName = ref(localStorage.getItem('store_name') || '');
|
||||||
|
|
||||||
|
|
||||||
|
const handleCommand = (command: string) => {
|
||||||
|
if (command === 'logout') {
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('store_id');
|
||||||
|
localStorage.removeItem('store_name');
|
||||||
|
token.value = '';
|
||||||
|
username.value = '';
|
||||||
|
selectedStoreName.value = '';
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => {
|
||||||
|
activeIndex.value = newPath;
|
||||||
|
// if (!token.value && newPath !== '/login') {
|
||||||
|
// router.push('/login');
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// if (!token.value) {
|
||||||
|
// router.push('/login');
|
||||||
|
// } else {
|
||||||
|
// selectedStoreName.value = localStorage.getItem('store_name') || '';
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
token,
|
||||||
|
isUserLoggedIn,
|
||||||
|
activeIndex,
|
||||||
|
handleCommand,
|
||||||
|
showDialog,
|
||||||
|
selectedStoreName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-header {
|
||||||
|
background-color: #1f2d3d;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
margin: 10px 20x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 110px;
|
||||||
|
margin-right: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-demo {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
/* justify-content: flex-start; */
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-demo .el-menu-item {
|
||||||
|
padding: 0 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-demo .el-menu-item:hover {
|
||||||
|
color: #00aaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info span {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card class="stats-card">
|
||||||
|
<div class="stats-header">告警数量和类型分布</div>
|
||||||
|
<el-row :gutter="20" class="stats-row">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div ref="chart" class="chart"></div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { login, getAlgorithms,getEvents} from '@/utils/superbox.js';
|
||||||
|
|
||||||
|
const username = 'turingvideo';
|
||||||
|
const password = '1234qwer!';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Statistics',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null,
|
||||||
|
seriesData: [
|
||||||
|
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// mounted() {
|
||||||
|
// this.initChart();
|
||||||
|
// },
|
||||||
|
async created(){
|
||||||
|
try{
|
||||||
|
const token = await login(username, password);
|
||||||
|
await this.fetchTypeMapping(token);
|
||||||
|
await this.updateSeriesData(token);
|
||||||
|
this.initChart();
|
||||||
|
}catch(error){
|
||||||
|
console.error("Error fetching algorithms:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchTypeMapping(token) {
|
||||||
|
const algorithms = await getAlgorithms(token);
|
||||||
|
let mapping = algorithms.map(algorithm => ({
|
||||||
|
value: 0, // 初始化为10,可以根据实际数据进行调整
|
||||||
|
code_name: algorithm.code_name,
|
||||||
|
name: algorithm.name
|
||||||
|
}));
|
||||||
|
const newMapping = [
|
||||||
|
{code_name: "minizawu:532",name: "杂物堆积",value: 0},
|
||||||
|
]
|
||||||
|
|
||||||
|
mapping = mapping.concat(newMapping);
|
||||||
|
this.seriesData = mapping;
|
||||||
|
},
|
||||||
|
async updateSeriesData(token){
|
||||||
|
const events = await getEvents(token);
|
||||||
|
events.forEach(event => {
|
||||||
|
const matchAlgorithm = this.seriesData.find(item => item.code_name === event.types);
|
||||||
|
if (matchAlgorithm){
|
||||||
|
matchAlgorithm.value += 1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initChart() {
|
||||||
|
this.chart = echarts.init(this.$refs.chart);
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'horizontal',
|
||||||
|
bottom: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
itemGap: 20,
|
||||||
|
data: this.seriesData.map(item => item.name),
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '告警类型',
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
center: ['50%', '150px'],
|
||||||
|
data: this.seriesData,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
stillShowZeroSum: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
this.chart.setOption(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stats-card {
|
||||||
|
background-color: #2a3f54;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #3a4b5c;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
/* margin-top: 20px; */
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,334 @@
|
||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="16">
|
||||||
|
<div class="table-container">
|
||||||
|
<el-table :data="paginatedData">
|
||||||
|
<el-table-column prop="id" label="告警编号" width="140"></el-table-column>
|
||||||
|
<el-table-column label="告警类型" width="140">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<!-- {{ typeMapping[scope.row.types] || codenameTranslate(scope.row.types) }} -->
|
||||||
|
{{ typeMapping[scope.row.types] }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="camera.name" label="告警位置" width="180"></el-table-column>
|
||||||
|
<el-table-column label="告警时间" width="180">
|
||||||
|
<template v-slot="scope">
|
||||||
|
{{ formatDateTime(scope.row.started_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="告警状态" width="140">
|
||||||
|
<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="操作" width="140">
|
||||||
|
<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 background layout="total, sizes, prev, pager, next, jumper" :total="tableData.length"
|
||||||
|
:page-size="pageSize" :current-page.sync="currentPage" @current-change="handlePageChange"
|
||||||
|
@size-change="handleSizeChange">
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="panel-section">
|
||||||
|
<statistics />
|
||||||
|
</div>
|
||||||
|
<div class="panel-section">
|
||||||
|
<alertChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
||||||
|
<div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>告警编号: {{ selectedRow.id }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<!-- <p>告警类型: {{ typeMapping[selectedRow.types] || codenameTranslate(selectedRow.types) }}</p> -->
|
||||||
|
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<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>
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import Statistics from '@/components/Statistics.vue';
|
||||||
|
import AlertChart from '@/components/AlertChart.vue';
|
||||||
|
import { login, getEvents, initCodeNameMap, codenameTranslate, getAlgorithms } from '@/utils/superbox.js';
|
||||||
|
|
||||||
|
const username = 'turingvideo';
|
||||||
|
const password = '1234qwer!';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AlertManagement',
|
||||||
|
components: {
|
||||||
|
Statistics,
|
||||||
|
AlertChart,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tableData: [],
|
||||||
|
dialogVisible: false,
|
||||||
|
selectedRow: {},
|
||||||
|
mediums: {},
|
||||||
|
duration: '',
|
||||||
|
typeMapping: {
|
||||||
|
// 'abnormal:525': '黑屏检测',
|
||||||
|
// 'car_blocking:512': '车辆拥塞',
|
||||||
|
// 'car_blocking:514': '违规停车',
|
||||||
|
// 'escalator_status:518': '扶梯运行状态',
|
||||||
|
// 'gathering:520': '人员密集',
|
||||||
|
// 'gathering:521': '保洁点名',
|
||||||
|
// 'intrude:513': '入侵检测',
|
||||||
|
// 'long_term_door_status:526': '长期门状态检测',
|
||||||
|
// 'lying_down:527': '人员倒地',
|
||||||
|
// 'minizawu:531': '杂物堆积',
|
||||||
|
// 'minizawu:532': '杂物堆积',
|
||||||
|
// 'personnel_stay:535': '人员逗留',
|
||||||
|
// 'vacant:524': '人员离岗',
|
||||||
|
// 'zawu:516': '饮料垃圾检测',
|
||||||
|
// 'zawu:517': '垃圾桶满溢',
|
||||||
|
// 'zawu:519': '违规放置',
|
||||||
|
// 'zawu:523': '绿化带垃圾检测',
|
||||||
|
},
|
||||||
|
statusMapping: {
|
||||||
|
'pending': '待处理',
|
||||||
|
'closed': '已处理'
|
||||||
|
},
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
const token = await login(username, password);
|
||||||
|
// await initCodeNameMap(token);
|
||||||
|
this.tableData = await getEvents(token);
|
||||||
|
this.typeMapping = await this.fetchTypeMapping(token);
|
||||||
|
console.log(this.tableData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching events:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
paginatedData() {
|
||||||
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
|
const end = start + this.pageSize;
|
||||||
|
return this.tableData.slice(start, end);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchTypeMapping(token) {
|
||||||
|
let algorithms = await getAlgorithms(token);
|
||||||
|
const additionalMappings = [
|
||||||
|
{ code_name: 'minizawu:532', name: '杂物堆积' },
|
||||||
|
|
||||||
|
];
|
||||||
|
algorithms = algorithms.concat(additionalMappings);
|
||||||
|
let mapping = {};
|
||||||
|
algorithms.forEach((algorithm) => {
|
||||||
|
mapping[algorithm.code_name] = algorithm.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log("mapping: ", mapping);
|
||||||
|
return mapping;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
handleView(row) {
|
||||||
|
this.selectedRow = row;
|
||||||
|
this.duration = this.calculateDuration(row.started_at, row.ended_at);
|
||||||
|
row.formatted_started_at = this.formatDateTime(row.started_at);
|
||||||
|
|
||||||
|
// console.log("duration: ", row.started_at);
|
||||||
|
// console.log("duration: ", row.ended_at);
|
||||||
|
// console.log("duration: ", this.duration);
|
||||||
|
this.dialogVisible = true;
|
||||||
|
this.mediums = row.mediums || {};
|
||||||
|
if (Array.isArray(row.mediums)) {
|
||||||
|
row.mediums.forEach(medium => {
|
||||||
|
// console.log("medium: ", medium.file);
|
||||||
|
// console.log("medium: ", medium.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// console.log("No mediums available");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handlePageChange(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
},
|
||||||
|
handleSizeChange(size) {
|
||||||
|
this.pageSize = size;
|
||||||
|
},
|
||||||
|
calculateDuration(started_at, ended_at) {
|
||||||
|
const start = new Date(started_at);
|
||||||
|
const end = new Date(ended_at);
|
||||||
|
const duration = end - start;
|
||||||
|
const minutes = Math.floor(duration / 60000);
|
||||||
|
const seconds = ((duration % 60000) / 1000).toFixed(0);
|
||||||
|
return `${minutes}分${(seconds < 10 ? '0' : '')}${seconds}秒`;
|
||||||
|
},
|
||||||
|
formatDateTime(datetime) {
|
||||||
|
const date = new Date(datetime);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home {
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-scroll {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-scroll-content {
|
||||||
|
/* 高度滚动条显示出来默认值 */
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
width: 80%;
|
||||||
|
max-height: 1000px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-left: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 10px 10px 400px 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗> 显示左右等分*/
|
||||||
|
.event-media {
|
||||||
|
/* display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px; */
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
/* 添加这个属性来左右等分 */
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-container {
|
||||||
|
flex: 0 0 48%;
|
||||||
|
/* 确保每块区域占据一行的48% */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
/* 使内容在区域内居中 */
|
||||||
|
margin-bottom: 20px;
|
||||||
|
/* 添加底部间距,使多个元素之间有间隔 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 确保padding和border不会影响大小 */
|
||||||
|
padding: 10px;
|
||||||
|
/* 添加内边距,使内容不靠近边框 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item video,
|
||||||
|
.image-item img {
|
||||||
|
width: 100%;
|
||||||
|
/* 保证视频和图片在容器内等宽 */
|
||||||
|
height: auto;
|
||||||
|
/* 保证视频和图片比例正常 */
|
||||||
|
border-radius: 8px;
|
||||||
|
/* 圆润效果 */
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
/* 阴影效果 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item p,
|
||||||
|
.image-item p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-right: 150px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,148 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {ElMessage} from 'element-plus'
|
||||||
|
import { Lock, User } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
import { useSuperbox } from '@/utils/Superbox'
|
||||||
|
|
||||||
|
import { useSetToken } from '@/utils/misc';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const superbox = useSuperbox();
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const setToken = useSetToken();
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const token = await superbox.login(loginForm.username, loginForm.password, false);
|
||||||
|
if (token) {
|
||||||
|
setToken(token);
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage(
|
||||||
|
{
|
||||||
|
message: '用户名或密码错误,登录失败',
|
||||||
|
type: 'error',
|
||||||
|
duration: 5000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReset = () => {
|
||||||
|
loginForm.username = '';
|
||||||
|
loginForm.password = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main-layout">
|
||||||
|
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="login-header">
|
||||||
|
<el-row justify="center">
|
||||||
|
<el-image src="./logo.png" fit="contain" />
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<div class="login-body">
|
||||||
|
<el-form :model="loginForm" style="max-width: 600px;">
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="loginForm.username" placeholder="用户名" clearable>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon class="el-input__icon">
|
||||||
|
<User />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="loginForm.password" placeholder="密码" type="password" show-password clearable>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon class="el-input__icon">
|
||||||
|
<Lock />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row justify="end">
|
||||||
|
<el-form-item size="small" :gutter="30">
|
||||||
|
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
||||||
|
<el-button type="primary" @click="onSubmit">登录</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<el-row justify="end">
|
||||||
|
<el-text class="plain-text">Powered by TuringAI</el-text>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-layout {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('./login-bg.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
min-width: 350px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #01111863;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-body {
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
background-color: #01111863;
|
||||||
|
border-radius: 10px 10px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
padding-right: 30px;
|
||||||
|
padding-left: 30px;
|
||||||
|
background-color: #01111863;
|
||||||
|
border-radius: 0px 0px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
color: #c2c8fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plain-text {
|
||||||
|
color: #9a9db3;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
nihao
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {createApp} from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.provide('axios', axios);
|
||||||
|
app.use(ElementPlus, { locale: zhCn });
|
||||||
|
app.use (router)
|
||||||
|
app.mount('#app')
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 第一步,引入createRouter
|
||||||
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import Layout from '@/components/Layout.vue';
|
||||||
|
import AlertChart from '@/components/AlertChart.vue';
|
||||||
|
import Statistics from '@/components/Statistics.vue';
|
||||||
|
import AlertManagement from '@/html/AlertManagement.vue';
|
||||||
|
import Test from '@/html/Test.vue';
|
||||||
|
// import Login from '@/html/LoginView.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Layout',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'AlertManagement',
|
||||||
|
component: AlertManagement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test',
|
||||||
|
name: 'Test',
|
||||||
|
component: Test,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/alertChart',
|
||||||
|
name: 'AlertChart',
|
||||||
|
component: AlertChart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/statistics',
|
||||||
|
name: 'Statistics',
|
||||||
|
component: Statistics,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
// ,
|
||||||
|
// {
|
||||||
|
// path: '/login',
|
||||||
|
// name: 'Login',
|
||||||
|
// component: Login,
|
||||||
|
// meta: { requiresAuth: false }
|
||||||
|
// },
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { SuperboxApi } from "@/utils/superboxApi";
|
||||||
|
|
||||||
|
const superbox = new SuperboxApi();
|
||||||
|
|
||||||
|
const useSuperbox = (address: string = "", port: number = 0) => {
|
||||||
|
superbox.setAddress(address);
|
||||||
|
superbox.setPort(port);
|
||||||
|
return superbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useSuperbox,
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// axios-config.ts
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
// baseURL: 'https://test1.turingvideo.cn/api/v1',
|
||||||
|
baseURL: 'http://127.0.0.1:8000/api/v1',
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/#/login'; // 重定向到登录页面
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default axiosInstance;
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const access_token = ref(null)
|
||||||
|
const consumerId = ref(null)
|
||||||
|
const data = ref(null)
|
||||||
|
|
||||||
|
// 计数器
|
||||||
|
let consumeMessageCount = 0
|
||||||
|
|
||||||
|
const getAccessToken = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/oauth/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: '88651e249e734a5290b00345961a7458',
|
||||||
|
client_secret: 'fbd457384ba0423cb5d6b86e4c7d3afc'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok')
|
||||||
|
const result = await response.json()
|
||||||
|
console.log("access_token:>>>>>>>>>", result)
|
||||||
|
access_token.value = result.access_token
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching access token:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createConsumer = async () => {
|
||||||
|
try {
|
||||||
|
const token = access_token.value
|
||||||
|
if (!token) throw new Error('Access token not found')
|
||||||
|
|
||||||
|
const response = await fetch('/api/api/v1/mq/consumer/group1', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok')
|
||||||
|
const result = await response.json()
|
||||||
|
console.log("createConsumer:>>>>>>>>>", result.data)
|
||||||
|
consumerId.value = result.data.consumerId
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating consumer:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumeMessage = async () => {
|
||||||
|
try {
|
||||||
|
const token = access_token.value
|
||||||
|
const cid = consumerId.value
|
||||||
|
|
||||||
|
if (!token || !cid) throw new Error('Token or Consumer ID not found')
|
||||||
|
|
||||||
|
const response = await fetch('/api/api/v1/mq/consumer/messages', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
autoCommit: 'true',
|
||||||
|
consumerId: cid
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok')
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
// 解析 content 数据
|
||||||
|
const parsedData = result.data.map(item => {
|
||||||
|
const content = JSON.parse(item.content.replace(/\\/g, '')) // 去除反斜杠
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
data.value = parsedData
|
||||||
|
console.log("consumeMessage:>>>>>>>>>", parsedData)
|
||||||
|
|
||||||
|
consumeMessageCount++
|
||||||
|
console.log(`consumeMessage request count: ${consumeMessageCount}`)
|
||||||
|
|
||||||
|
return parsedData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error consuming message:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时器管理
|
||||||
|
let refreshTokenIntervalId = null
|
||||||
|
let consumeMessageIntervalId = null
|
||||||
|
|
||||||
|
const startAutoRefresh = () => {
|
||||||
|
// 1小时刷新一次 getAccessToken 和 createConsumer
|
||||||
|
refreshTokenIntervalId = setInterval(async () => {
|
||||||
|
await getAccessToken()
|
||||||
|
await createConsumer()
|
||||||
|
}, 3600000) // 3600000ms = 1小时
|
||||||
|
|
||||||
|
// 每25秒触发一次 consumeMessage
|
||||||
|
consumeMessageIntervalId = setInterval(async () => {
|
||||||
|
await consumeMessage()
|
||||||
|
}, 25000) // 25000ms = 25秒
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAutoRefresh = () => {
|
||||||
|
if (refreshTokenIntervalId) clearInterval(refreshTokenIntervalId)
|
||||||
|
if (consumeMessageIntervalId) clearInterval(consumeMessageIntervalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
access_token,
|
||||||
|
consumerId,
|
||||||
|
data,
|
||||||
|
getAccessToken,
|
||||||
|
createConsumer,
|
||||||
|
consumeMessage,
|
||||||
|
startAutoRefresh,
|
||||||
|
stopAutoRefresh
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
const JWT_TOKEN = 'token';
|
||||||
|
|
||||||
|
const useGetToken = (failedProc: () => void) => {
|
||||||
|
const getToken = (): string => {
|
||||||
|
const token = sessionStorage.getItem(JWT_TOKEN);
|
||||||
|
if (!token) {
|
||||||
|
failedProc();
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
return getToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSetToken = () => {
|
||||||
|
const setToken = (token: string) => {
|
||||||
|
sessionStorage.setItem(JWT_TOKEN, token);
|
||||||
|
}
|
||||||
|
return setToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const useClearToken = () => {
|
||||||
|
const clearToken = () => {
|
||||||
|
sessionStorage.removeItem(JWT_TOKEN);
|
||||||
|
}
|
||||||
|
return clearToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGetToken, useSetToken, useClearToken }
|
|
@ -0,0 +1,160 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const getSuperboxApiConfig = (address) => {
|
||||||
|
const PORT = 8000;
|
||||||
|
const BASE_ROUTE = "/api/v1";
|
||||||
|
const LOGIN_ROUTE = BASE_ROUTE + "/auth/login";
|
||||||
|
const LOGOUT_ROUTE = BASE_ROUTE + "/auth/logout";
|
||||||
|
const CAMERA_ROUTE = BASE_ROUTE + "/camera/cameras";
|
||||||
|
const EVENTS_ROUTE = BASE_ROUTE + "/event/events?limit=300";
|
||||||
|
const ALGORITHM_ROUTE = BASE_ROUTE + "/algorithms";
|
||||||
|
|
||||||
|
let addr = address;
|
||||||
|
if (!addr) {
|
||||||
|
addr = window.location.host.replace(/:\d+/, "");
|
||||||
|
console.log("No address provided, using " + addr);
|
||||||
|
}
|
||||||
|
const superboxAddress = "http://" + addr + ":" + PORT.toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: superboxAddress + BASE_ROUTE,
|
||||||
|
login: superboxAddress + LOGIN_ROUTE,
|
||||||
|
logout: superboxAddress + LOGOUT_ROUTE,
|
||||||
|
cameras: superboxAddress + CAMERA_ROUTE,
|
||||||
|
events: superboxAddress + EVENTS_ROUTE,
|
||||||
|
algorithms: superboxAddress + ALGORITHM_ROUTE,
|
||||||
|
};
|
||||||
|
/*return {
|
||||||
|
base: "/api/v1",
|
||||||
|
login: "/api/v1/auth/login",
|
||||||
|
logout: "/api/v1/auth/logout",
|
||||||
|
cameras: "/api/v1/camera/cameras",
|
||||||
|
events: "/api/v1/event/events",
|
||||||
|
};*/
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.defaults.withCredentials = true;
|
||||||
|
export const login = (username, password, address = null) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const api = getSuperboxApiConfig(address);
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
api.login,
|
||||||
|
{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
cookieless: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.err.ec === 0) {
|
||||||
|
resolve(res.data.ret.token);
|
||||||
|
}
|
||||||
|
reject(res.data.err);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCameras = (token, address = null) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const api = getSuperboxApiConfig(address);
|
||||||
|
axios
|
||||||
|
.get(api.cameras, {
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.err.ec === 0) {
|
||||||
|
resolve(res.data.ret.results);
|
||||||
|
}
|
||||||
|
reject(res.data.err);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEvents = (token, address = null) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const api = getSuperboxApiConfig(address);
|
||||||
|
axios
|
||||||
|
.get(api.events, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.err.ec === 0) {
|
||||||
|
resolve(res.data.ret.results);
|
||||||
|
}
|
||||||
|
reject(res.data.err);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAlgorithms = (token, address = null) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const api = getSuperboxApiConfig(address);
|
||||||
|
axios
|
||||||
|
.get(api.algorithms, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.err.ec === 0) {
|
||||||
|
resolve(res.data.ret);
|
||||||
|
}
|
||||||
|
reject(res.data.err);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var codeNameMap = {};
|
||||||
|
|
||||||
|
export const initCodeNameMap = async (token, address = null) => {
|
||||||
|
try {
|
||||||
|
const algorithms = await getAlgorithms(token, address);
|
||||||
|
algorithms.forEach((algorithm) => {
|
||||||
|
codeNameMap[algorithm.code_name] = algorithm.name;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codenameTranslate = (codeName) => {
|
||||||
|
return codeNameMap[codeName];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login,
|
||||||
|
getCameras,
|
||||||
|
getEvents,
|
||||||
|
getSuperboxApiConfig,
|
||||||
|
initCodeNameMap,
|
||||||
|
codenameTranslate,
|
||||||
|
getAlgorithms,
|
||||||
|
};
|
|
@ -0,0 +1,200 @@
|
||||||
|
/* eslint-disable no-useless-catch */
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
class SuperboxApi {
|
||||||
|
|
||||||
|
private readonly defaultPort: number = 8000;
|
||||||
|
|
||||||
|
private readonly apiLogin: string = "/auth/login";
|
||||||
|
private readonly apiLogout: string = "/auth/logout";
|
||||||
|
private readonly apiCameras: string = "/cameras";
|
||||||
|
private readonly apiEvents: string = "/events";
|
||||||
|
private readonly apiAlgorithms: string = "/algorithms";
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
this.setAddress(address);
|
||||||
|
this.setPort(port);
|
||||||
|
this.axios = axios.create({
|
||||||
|
baseURL: `http://${this.address}:${this.port}/api/v1`,
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAddress(address: string) {
|
||||||
|
this.address = address === "" ? this._boxAddr() : address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPort(port: number) {
|
||||||
|
this.port = port === 0 ? this.defaultPort : port;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 getOneEvent(token: string | null = null): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await this.getEvents(1, 0, token);
|
||||||
|
} 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;
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _boxAddr() {
|
||||||
|
return window.location.host.replace(/:\d+/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SuperboxApi };
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server:{
|
||||||
|
// host:'192.168.28.44',
|
||||||
|
host:'127.0.0.1',
|
||||||
|
// host: '192.168.28.32',
|
||||||
|
port: 5173,
|
||||||
|
open:true,
|
||||||
|
cors: true,
|
||||||
|
// proxy: {
|
||||||
|
// '/api': "192.168.28.44:8000"
|
||||||
|
// }
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8000', // 目标 API 地址
|
||||||
|
changeOrigin: true, // 开启跨域
|
||||||
|
rewrite: path => path.replace('^/api/', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in New Issue