本地告警页面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