本地告警页面V0

This commit is contained in:
龚皓 2024-09-04 16:23:57 +08:00
commit c9ba2ad556
29 changed files with 3831 additions and 0 deletions

30
.gitignore vendored Normal file
View File

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

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

260
README.md Normal file
View File

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

8
env.d.ts vendored Normal file
View File

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

13
index.html Normal file
View File

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

1730
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

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

BIN
public/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/login-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

17
src/App.vue Normal file
View File

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

View File

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

167
src/components/Layout.vue Normal file
View File

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

View File

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

View File

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

148
src/html/LoginView.vue Normal file
View File

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

12
src/html/Test.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<div>
nihao
</div>
</template>
<script lang="ts">
</script>
<style scoped>
</style>

16
src/main.ts Normal file
View File

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

58
src/router/index.ts Normal file
View File

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

13
src/utils/Superbox.ts Normal file
View File

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

36
src/utils/axios-config.ts Normal file
View File

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

View File

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

30
src/utils/misc.ts Normal file
View File

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

160
src/utils/superbox.js Normal file
View File

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

200
src/utils/superboxApi.ts Normal file
View File

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

14
tsconfig.app.json Normal file
View File

@ -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/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -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"]
}
}

34
vite.config.ts Normal file
View File

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