fix: 首页数据大屏联调接口
This commit is contained in:
parent
b71a9b398d
commit
859dbbb1c7
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@kjgl77/datav-vue3": "^1.7.4",
|
"@kjgl77/datav-vue3": "^1.7.4",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"echarts-liquidfill": "^3.1.0",
|
"echarts-liquidfill": "^3.1.0",
|
||||||
"element-plus": "^2.10.4",
|
"element-plus": "^2.10.4",
|
||||||
|
232
web/src/App.vue
232
web/src/App.vue
@ -23,12 +23,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="center-section">
|
<div class="center-section">
|
||||||
<AlarmTrend />
|
<AlarmTrend />
|
||||||
<CameraStats />
|
<CameraStats
|
||||||
|
:total-cameras="cameraStats.total_cameras"
|
||||||
|
:online-cameras="cameraStats.online_cameras"
|
||||||
|
:total-algorithms="algorithmStats.total_algorithms"
|
||||||
|
:active-algorithms="algorithmStats.active_algorithms"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-section">
|
<div class="right-section">
|
||||||
<dv-border-box-1>
|
<dv-border-box-1>
|
||||||
<EventHotSpots />
|
<EventHotSpots :locations="cameraStats.by_location" />
|
||||||
<AlgorithmStats />
|
<AlgorithmStats :algorithm-types="algorithmStats.by_type" />
|
||||||
</dv-border-box-1>
|
</dv-border-box-1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -80,13 +85,57 @@ import AlgorithmCenter from './components/algorithm/AlgorithmCenter.vue'
|
|||||||
import AlarmManagement from './components/alarm/AlarmManagement.vue'
|
import AlarmManagement from './components/alarm/AlarmManagement.vue'
|
||||||
import EventCenter from './components/event/EventCenter.vue'
|
import EventCenter from './components/event/EventCenter.vue'
|
||||||
import DeviceManagement from './components/device/DeviceManagement.vue'
|
import DeviceManagement from './components/device/DeviceManagement.vue'
|
||||||
|
import API from '@/api/index'
|
||||||
const activeTab = ref('overview')
|
const activeTab = ref('overview')
|
||||||
const dashboardContainer = ref(null)
|
const dashboardContainer = ref(null)
|
||||||
const isFullscreen = ref(false)
|
const isFullscreen = ref(false)
|
||||||
|
|
||||||
const showMonitorDetail = ref(false)
|
const showMonitorDetail = ref(false)
|
||||||
|
|
||||||
|
const cameraStats = ref({
|
||||||
|
total_cameras: 0,
|
||||||
|
online_cameras: 0,
|
||||||
|
offline_cameras: 0,
|
||||||
|
by_location: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const algorithmStats = ref({
|
||||||
|
total_algorithms: 0,
|
||||||
|
active_algorithms: 0,
|
||||||
|
by_type: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取摄像头统计数据
|
||||||
|
const fetchCameraStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await API.getCameraStats()
|
||||||
|
cameraStats.value = response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取摄像头统计数据失败:', error)
|
||||||
|
cameraStats.value = {
|
||||||
|
total_cameras: 0,
|
||||||
|
online_cameras: 0,
|
||||||
|
offline_cameras: 0,
|
||||||
|
by_location: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取算法统计数据
|
||||||
|
const fetchAlgorithmStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await API.getAlgorithmStats()
|
||||||
|
algorithmStats.value = response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取算法统计数据失败:', error)
|
||||||
|
algorithmStats.value = {
|
||||||
|
total_algorithms: 0,
|
||||||
|
active_algorithms: 0,
|
||||||
|
by_type: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentMonitor = ref({
|
const currentMonitor = ref({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: '港口区主监控',
|
name: '港口区主监控',
|
||||||
@ -94,91 +143,95 @@ const currentMonitor = ref({
|
|||||||
status: 'online',
|
status: 'online',
|
||||||
videoSrc: '/videos/port-main.mp4'
|
videoSrc: '/videos/port-main.mp4'
|
||||||
})
|
})
|
||||||
|
const monitors = ref([])
|
||||||
|
// const monitors = ref([
|
||||||
|
// {
|
||||||
|
// id: 1,
|
||||||
|
// name: '港口区监控1',
|
||||||
|
// location: '港口A区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-1.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'person', x: 25, y: 35, width: 40, height: 80 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 2,
|
||||||
|
// name: '港口区监控2',
|
||||||
|
// location: '港口B区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-2.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 3,
|
||||||
|
// name: '港口区监控2',
|
||||||
|
// location: '港口B区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-2.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 4,
|
||||||
|
// name: '港口区监控2',
|
||||||
|
// location: '港口B区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-2.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 5,
|
||||||
|
// name: '港口区监控1',
|
||||||
|
// location: '港口A区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-1.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'person', x: 25, y: 35, width: 40, height: 80 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 6,
|
||||||
|
// name: '港口区监控2',
|
||||||
|
// location: '港口B区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-2.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 7,
|
||||||
|
// name: '港口区监控2',
|
||||||
|
// location: '港口B区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-2.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 8,
|
||||||
|
// name: '港口区监控2',
|
||||||
|
// location: '港口B区',
|
||||||
|
// status: 'online',
|
||||||
|
// videoSrc: '/videos/port-2.mp4',
|
||||||
|
// detections: [
|
||||||
|
// { type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
|
||||||
const monitors = ref([
|
// ])
|
||||||
{
|
// 获取列表
|
||||||
id: 1,
|
const getMonitorsList = async () => {
|
||||||
name: '港口区监控1',
|
const list = await API.getMonitorsList()
|
||||||
location: '港口A区',
|
monitors.value = list.monitors || []
|
||||||
status: 'online',
|
}
|
||||||
videoSrc: '/videos/port-1.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'person', x: 25, y: 35, width: 40, height: 80 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '港口区监控2',
|
|
||||||
location: '港口B区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-2.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '港口区监控2',
|
|
||||||
location: '港口B区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-2.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '港口区监控2',
|
|
||||||
location: '港口B区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-2.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: '港口区监控1',
|
|
||||||
location: '港口A区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-1.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'person', x: 25, y: 35, width: 40, height: 80 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: '港口区监控2',
|
|
||||||
location: '港口B区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-2.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: '港口区监控2',
|
|
||||||
location: '港口B区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-2.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: '港口区监控2',
|
|
||||||
location: '港口B区',
|
|
||||||
status: 'online',
|
|
||||||
videoSrc: '/videos/port-2.mp4',
|
|
||||||
detections: [
|
|
||||||
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
// 切换标签
|
// 切换标签
|
||||||
const handleTabChange = (tab) => {
|
const handleTabChange = (tab) => {
|
||||||
@ -217,6 +270,9 @@ const handleFullscreenChange = () => {
|
|||||||
|
|
||||||
// 添加事件监听
|
// 添加事件监听
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getMonitorsList()
|
||||||
|
fetchCameraStats()
|
||||||
|
fetchAlgorithmStats()
|
||||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,11 +1,89 @@
|
|||||||
import http from '../utils/http'
|
import http from '../utils/http'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login(username, password) {
|
getKpi() {
|
||||||
return http.post('/user/login', { username, password })
|
return http.get('/dashboard/kpi')
|
||||||
},
|
},
|
||||||
|
|
||||||
getInfo() {
|
getAlarmTrend() {
|
||||||
return http.get('/user/info')
|
return http.get('/dashboard/alarm-trend')
|
||||||
}
|
},
|
||||||
|
|
||||||
|
getCameraStats() {
|
||||||
|
return http.get('/dashboard/camera-stats')
|
||||||
|
},
|
||||||
|
|
||||||
|
getAlgorithmStats() {
|
||||||
|
return http.get('/dashboard/algorithm-stats')
|
||||||
|
},
|
||||||
|
|
||||||
|
getMonitorsList(params) {
|
||||||
|
return http.get('/monitors/', params)
|
||||||
|
},
|
||||||
|
getMonitorsDetail(monitor_id) {
|
||||||
|
return http.get(`/api/monitors/${monitor_id}`)
|
||||||
|
},
|
||||||
|
// 获取场景列表
|
||||||
|
getScenesList(params) {
|
||||||
|
return http.get('/scenes/', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加场景
|
||||||
|
addScenes(data) {
|
||||||
|
return http.post('/scenes/', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备管理接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 添加设备
|
||||||
|
addDevices(data) {
|
||||||
|
return http.post('/devices/', data)
|
||||||
|
},
|
||||||
|
// 获取设备列表
|
||||||
|
getDevices(params) {
|
||||||
|
return http.get('/devices/', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传视频文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传视频文件(二进制格式)
|
||||||
|
* @param {File} file - 视频文件
|
||||||
|
* @param {string} deviceId - 设备ID
|
||||||
|
* @param {string} [description] - 视频描述
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
uploadVideo (file, deviceId, description = '') {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
// 读取文件为二进制字符串
|
||||||
|
reader.readAsBinaryString(file)
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
const binaryString = reader.result
|
||||||
|
|
||||||
|
request({
|
||||||
|
url: '/upload/video',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
file: binaryString, // 二进制字符串
|
||||||
|
device_id: deviceId,
|
||||||
|
description: description
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' // 使用JSON格式
|
||||||
|
}
|
||||||
|
}).then(resolve).catch(reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = error => reject(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -10,12 +10,57 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import API from '@/api/index'
|
||||||
|
|
||||||
const chart = ref(null)
|
const chart = ref(null)
|
||||||
|
let myChart = null
|
||||||
|
|
||||||
|
const fetchAlarmTrend = async () => {
|
||||||
|
try {
|
||||||
|
const response = await API.getAlarmTrend()
|
||||||
|
const trendData = response
|
||||||
|
|
||||||
|
// 格式化月份数据,只显示月份部分
|
||||||
|
const formattedMonths = trendData.months.map(month => {
|
||||||
|
return month.split('-')[1] + '月'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新图表选项
|
||||||
|
const option = {
|
||||||
|
xAxis: {
|
||||||
|
data: formattedMonths
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'P0告警',
|
||||||
|
data: trendData.p0_warnings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P1告警',
|
||||||
|
data: trendData.p1_warnings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P2告警',
|
||||||
|
data: trendData.p2_warnings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P3告警',
|
||||||
|
data: trendData.p3_warnings
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
myChart.setOption(option)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取告警趋势数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const myChart = echarts.init(chart.value)
|
myChart = echarts.init(chart.value)
|
||||||
|
|
||||||
|
// 初始化默认选项
|
||||||
const option = {
|
const option = {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -44,7 +89,7 @@ onMounted(() => {
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: ['1月', '2月', '3月', '4月', '5月'],
|
data: [], // 初始为空,将从API获取
|
||||||
axisLine: { lineStyle: { color: 'rgba(124, 208, 255, 0.5)' } },
|
axisLine: { lineStyle: { color: 'rgba(124, 208, 255, 0.5)' } },
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'rgba(124, 208, 255, 0.8)',
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
@ -55,8 +100,8 @@ onMounted(() => {
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 550,
|
max: 60, // 根据数据范围调整
|
||||||
interval: 50,
|
interval: 10,
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: { color: 'rgba(124, 208, 255, 0.5)' }
|
lineStyle: { color: 'rgba(124, 208, 255, 0.5)' }
|
||||||
},
|
},
|
||||||
@ -84,7 +129,7 @@ onMounted(() => {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#fff'
|
borderColor: '#fff'
|
||||||
},
|
},
|
||||||
data: [300, 100, 400, 250, 200]
|
data: [] // 初始为空,将从API获取
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P1告警',
|
name: 'P1告警',
|
||||||
@ -98,7 +143,7 @@ onMounted(() => {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#fff'
|
borderColor: '#fff'
|
||||||
},
|
},
|
||||||
data: [300, 100, 400, 250, 200]
|
data: [] // 初始为空,将从API获取
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P2告警',
|
name: 'P2告警',
|
||||||
@ -112,7 +157,7 @@ onMounted(() => {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#fff'
|
borderColor: '#fff'
|
||||||
},
|
},
|
||||||
data: [350, 150, 550, 50, 110]
|
data: [] // 初始为空,将从API获取
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P3告警',
|
name: 'P3告警',
|
||||||
@ -126,13 +171,16 @@ onMounted(() => {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#fff'
|
borderColor: '#fff'
|
||||||
},
|
},
|
||||||
data: [200, 300, 400, 100, 50]
|
data: [] // 初始为空,将从API获取
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
myChart.setOption(option)
|
myChart.setOption(option)
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
fetchAlarmTrend()
|
||||||
|
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
myChart.resize()
|
myChart.resize()
|
||||||
})
|
})
|
||||||
@ -142,17 +190,16 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.alert-trend-container {
|
.alert-trend-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50vh; /* 增加高度 */
|
height: 50vh;
|
||||||
padding: 15px 10px;
|
padding: 15px 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-top: 50px; /* 增加上方间距 */
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-box {
|
.border-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
/* padding: 20px; */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,75 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
const radarChart = ref(null)
|
const props = defineProps({
|
||||||
|
algorithmTypes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const radarChart = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
// 颜色数组
|
||||||
|
const colors = [
|
||||||
|
{ line: '#A020F0', area: 'rgba(160, 32, 240, 0.2)' }, // 紫色
|
||||||
|
{ line: '#1E90FF', area: 'rgba(30, 144, 255, 0.2)' }, // 蓝色
|
||||||
|
{ line: '#FF6347', area: 'rgba(255, 99, 71, 0.2)' }, // 红色
|
||||||
|
{ line: '#32CD32', area: 'rgba(50, 205, 50, 0.2)' }, // 绿色
|
||||||
|
{ line: '#FFD700', area: 'rgba(255, 215, 0, 0.2)' } // 金色
|
||||||
|
]
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!radarChart.value) return
|
||||||
|
|
||||||
|
chartInstance = echarts.init(radarChart.value)
|
||||||
|
|
||||||
|
// 固定雷达图维度
|
||||||
|
const indicators = [
|
||||||
|
{ name: '命中率', max: 100 },
|
||||||
|
{ name: '误报率', max: 100 },
|
||||||
|
{ name: '触发器', max: 100 },
|
||||||
|
{ name: '处理时延', max: 100 },
|
||||||
|
{ name: '稳定性', max: 100 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 根据算法类型生成系列数据
|
||||||
|
const seriesData = props.algorithmTypes.map((item, index) => {
|
||||||
|
// 计算各维度值
|
||||||
|
const values = [
|
||||||
|
item.accuracy, // 命中率 = 准确率
|
||||||
|
100 - item.accuracy, // 误报率 = 100 - 准确率
|
||||||
|
Math.min(100, item.count), // 触发器 = count * 15 (最大100)
|
||||||
|
100 - Math.round(item.accuracy / 1.5), // 处理时延 = 反比于准确率
|
||||||
|
90 + Math.round(item.accuracy / 10) // 稳定性 = 90 + 准确率/10
|
||||||
|
]
|
||||||
|
|
||||||
|
const colorIndex = index % colors.length
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: values,
|
||||||
|
name: item.type,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: colors[colorIndex].line
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: colors[colorIndex].line,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: colors[colorIndex].area
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const radarOption = {
|
const radarOption = {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -24,10 +87,20 @@ onMounted(() => {
|
|||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12
|
||||||
|
},
|
||||||
|
formatter: params => {
|
||||||
|
return `
|
||||||
|
<div style="font-weight:bold">${params.name}</div>
|
||||||
|
<div>命中率: ${params.value[0].toFixed(1)}%</div>
|
||||||
|
<div>误报率: ${params.value[1].toFixed(1)}%</div>
|
||||||
|
<div>触发器: ${params.value[2].toFixed(0)}</div>
|
||||||
|
<div>处理时延: ${params.value[3].toFixed(0)}ms</div>
|
||||||
|
<div>稳定性: ${params.value[4].toFixed(0)}%</div>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: ['目标识别算法', '行为识别算法'],
|
data: props.algorithmTypes.map(item => item.type),
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
@ -38,13 +111,7 @@ onMounted(() => {
|
|||||||
itemGap: 20
|
itemGap: 20
|
||||||
},
|
},
|
||||||
radar: {
|
radar: {
|
||||||
indicator: [
|
indicator: indicators,
|
||||||
{ name: '命中率', max: 100 },
|
|
||||||
{ name: '误报率', max: 100 },
|
|
||||||
{ name: '触发器', max: 100 },
|
|
||||||
{ name: '处理时延', max: 100 },
|
|
||||||
{ name: '稳定性', max: 100 }
|
|
||||||
],
|
|
||||||
radius: '65%',
|
radius: '65%',
|
||||||
axisName: {
|
axisName: {
|
||||||
color: 'rgba(124, 208, 255, 0.8)',
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
@ -69,60 +136,32 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
type: 'radar',
|
type: 'radar',
|
||||||
data: [
|
data: seriesData
|
||||||
{
|
|
||||||
value: [90, 85, 95, 80, 88],
|
|
||||||
name: '目标识别算法',
|
|
||||||
symbol: 'circle',
|
|
||||||
symbolSize: 6,
|
|
||||||
lineStyle: {
|
|
||||||
width: 2,
|
|
||||||
color: '#A020F0' // 紫色
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#A020F0',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#fff'
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: 'rgba(160, 32, 240, 0.2)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: [80, 75, 85, 70, 82],
|
|
||||||
name: '行为识别算法',
|
|
||||||
symbol: 'circle',
|
|
||||||
symbolSize: 6,
|
|
||||||
lineStyle: {
|
|
||||||
width: 2,
|
|
||||||
color: '#1E90FF' // 蓝色
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#1E90FF',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#fff'
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: 'rgba(30, 144, 255, 0.2)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartInstance = echarts.init(radarChart.value)
|
|
||||||
chartInstance.setOption(radarOption)
|
chartInstance.setOption(radarOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initChart()
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
chartInstance.resize()
|
chartInstance && chartInstance.resize()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.algorithmTypes, () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
initChart()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.radar-chart-container {
|
.radar-chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh; /* 高度占一半 */
|
height: 40vh;
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -131,7 +170,6 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
/* padding: 20px; */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
<div class="gauge-container">
|
<div class="gauge-container">
|
||||||
<div class="gauge">
|
<div class="gauge">
|
||||||
<div ref="onlineRateChart" class="gauge-chart"></div>
|
<div ref="onlineRateChart" class="gauge-chart"></div>
|
||||||
<div class="gauge-value">58%</div>
|
<div class="gauge-value">{{ onlineRate }}%</div>
|
||||||
<div class="gauge-label">摄像头在线率</div>
|
<div class="gauge-label">摄像头在线率</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gauge">
|
<div class="gauge">
|
||||||
<div ref="coverageChart" class="gauge-chart"></div>
|
<div ref="coverageChart" class="gauge-chart"></div>
|
||||||
<div class="gauge-value">40%</div>
|
<div class="gauge-value">{{ coverageRate }}%</div>
|
||||||
<div class="gauge-label">算法部署覆盖率</div>
|
<div class="gauge-label">算法部署覆盖率</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -19,59 +19,115 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import 'echarts-liquidfill' // 引入水波图扩展
|
import 'echarts-liquidfill'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
totalCameras: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
onlineCameras: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
totalAlgorithms: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
activeAlgorithms: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const onlineRateChart = ref(null)
|
const onlineRateChart = ref(null)
|
||||||
const coverageChart = ref(null)
|
const coverageChart = ref(null)
|
||||||
|
|
||||||
|
// 计算摄像头在线率
|
||||||
|
const onlineRate = computed(() => {
|
||||||
|
if (props.totalCameras === 0) return 0
|
||||||
|
return Math.round((props.onlineCameras / props.totalCameras) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算算法部署覆盖率
|
||||||
|
const coverageRate = computed(() => {
|
||||||
|
if (props.totalAlgorithms === 0) return 0
|
||||||
|
return Math.round((props.activeAlgorithms / props.totalAlgorithms) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 水波图配置
|
||||||
|
const createLiquidOption = (value, color) => ({
|
||||||
|
series: [{
|
||||||
|
type: 'liquidFill',
|
||||||
|
radius: '70%',
|
||||||
|
center: ['50%', '50%'],
|
||||||
|
data: [value / 100 * 0.9, value / 100 * 0.7],
|
||||||
|
color: [color],
|
||||||
|
backgroundStyle: {
|
||||||
|
color: 'rgba(16, 42, 87, 0.5)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(124, 208, 255, 0.3)'
|
||||||
|
},
|
||||||
|
outline: { show: false },
|
||||||
|
label: { show: false },
|
||||||
|
waveAnimation: true,
|
||||||
|
animationDuration: 2000,
|
||||||
|
animationEasing: 'linear',
|
||||||
|
amplitude: 6,
|
||||||
|
waveLength: '80%'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
const updateCharts = () => {
|
||||||
|
if (onlineRateChart.value && coverageChart.value) {
|
||||||
|
const onlineRateChartInstance = echarts.getInstanceByDom(onlineRateChart.value) ||
|
||||||
|
echarts.init(onlineRateChart.value)
|
||||||
|
onlineRateChartInstance.setOption(createLiquidOption(onlineRate.value, '#3A8BFF'))
|
||||||
|
|
||||||
|
const coverageChartInstance = echarts.getInstanceByDom(coverageChart.value) ||
|
||||||
|
echarts.init(coverageChart.value)
|
||||||
|
coverageChartInstance.setOption(createLiquidOption(coverageRate.value, '#FFA726'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化
|
||||||
|
watch([onlineRate, coverageRate], () => {
|
||||||
|
updateCharts()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 水波图配置
|
updateCharts()
|
||||||
const createLiquidOption = (color) => ({
|
|
||||||
series: [{
|
const resizeHandler = () => {
|
||||||
type: 'liquidFill',
|
[onlineRateChart.value, coverageChart.value].forEach(chart => {
|
||||||
radius: '70%',
|
if (chart) {
|
||||||
center: ['50%', '50%'],
|
const instance = echarts.getInstanceByDom(chart)
|
||||||
data: [0.338, 0.48], // 水波高度
|
instance && instance.resize()
|
||||||
color: [color],
|
}
|
||||||
backgroundStyle: {
|
})
|
||||||
color: 'rgba(16, 42, 87, 0.5)',
|
}
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(124, 208, 255, 0.3)'
|
window.addEventListener('resize', resizeHandler)
|
||||||
},
|
|
||||||
outline: {
|
return () => {
|
||||||
show: false
|
window.removeEventListener('resize', resizeHandler)
|
||||||
},
|
[onlineRateChart.value, coverageChart.value].forEach(chart => {
|
||||||
label: {
|
if (chart) {
|
||||||
show: false
|
const instance = echarts.getInstanceByDom(chart)
|
||||||
},
|
instance && instance.dispose()
|
||||||
waveAnimation: true,
|
}
|
||||||
animationDuration: 2000,
|
})
|
||||||
animationEasing: 'linear',
|
}
|
||||||
amplitude: 6,
|
|
||||||
waveLength: '80%'
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化图表
|
|
||||||
const onlineRate = echarts.init(onlineRateChart.value)
|
|
||||||
onlineRate.setOption(createLiquidOption('#3A8BFF'))
|
|
||||||
|
|
||||||
const coverage = echarts.init(coverageChart.value)
|
|
||||||
coverage.setOption(createLiquidOption('#FFA726'))
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
onlineRate.resize()
|
|
||||||
coverage.resize()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 样式保持不变 */
|
/* 样式保持不变 */
|
||||||
.stats-container {
|
.stats-container {
|
||||||
|
|
||||||
height: 30vh;
|
height: 30vh;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 15px 10px;
|
padding: 15px 10px;
|
||||||
|
@ -8,12 +8,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
const barChart = ref(null)
|
const props = defineProps({
|
||||||
|
locations: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
const barChart = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!barChart.value) return
|
||||||
|
|
||||||
|
chartInstance = echarts.init(barChart.value)
|
||||||
|
|
||||||
const barOption = {
|
const barOption = {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -25,9 +37,9 @@ onMounted(() => {
|
|||||||
textStyle: { color: '#fff' }
|
textStyle: { color: '#fff' }
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: ['检测数据1', '检测数据2'],
|
data: ['摄像头总数', '在线数量'],
|
||||||
bottom: 10, // 距离底部10px
|
bottom: 10,
|
||||||
left: 'center', // 水平居中
|
left: 'center',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12
|
||||||
@ -39,15 +51,15 @@ onMounted(() => {
|
|||||||
grid: {
|
grid: {
|
||||||
left: '2%',
|
left: '2%',
|
||||||
right: '4%',
|
right: '4%',
|
||||||
bottom: '15%', // 增加底部间距给图例留空间
|
bottom: '15%',
|
||||||
top: '5%',
|
top: '5%',
|
||||||
containLabel: true
|
containLabel: true
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 250,
|
max: Math.max(...props.locations.map(l => l.total)) + 10,
|
||||||
interval: 50,
|
interval: Math.ceil(Math.max(...props.locations.map(l => l.total)) / 5),
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(124, 208, 255, 0.5)'
|
color: 'rgba(124, 208, 255, 0.5)'
|
||||||
@ -66,13 +78,7 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: [
|
data: props.locations.map(l => l.location),
|
||||||
'办公走廊/楼道区',
|
|
||||||
'会议室区域',
|
|
||||||
'值班岗亭区',
|
|
||||||
'边检通道区',
|
|
||||||
'港口泊位区'
|
|
||||||
],
|
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(124, 208, 255, 0.5)'
|
color: 'rgba(124, 208, 255, 0.5)'
|
||||||
@ -88,7 +94,7 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '检测数据1',
|
name: '摄像头总数',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
barWidth: 15,
|
barWidth: 15,
|
||||||
label: {
|
label: {
|
||||||
@ -102,10 +108,10 @@ onMounted(() => {
|
|||||||
color: '#FFA500',
|
color: '#FFA500',
|
||||||
borderRadius: [0, 4, 4, 0]
|
borderRadius: [0, 4, 4, 0]
|
||||||
},
|
},
|
||||||
data: [120, 150, 200, 100, 150]
|
data: props.locations.map(l => l.total)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '检测数据2',
|
name: '在线数量',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
barWidth: 15,
|
barWidth: 15,
|
||||||
label: {
|
label: {
|
||||||
@ -119,24 +125,33 @@ onMounted(() => {
|
|||||||
color: '#00FFC0',
|
color: '#00FFC0',
|
||||||
borderRadius: [0, 4, 4, 0]
|
borderRadius: [0, 4, 4, 0]
|
||||||
},
|
},
|
||||||
data: [100, 120, 100, 80, 100]
|
data: props.locations.map(l => l.online)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartInstance = echarts.init(barChart.value)
|
|
||||||
chartInstance.setOption(barOption)
|
chartInstance.setOption(barOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initChart()
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
chartInstance.resize()
|
chartInstance && chartInstance.resize()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.locations, () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
initChart()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.half-height-chart-container {
|
.half-height-chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh; /* 关键修改:高度设为视口高度的50% */
|
height: 40vh;
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@ -146,7 +161,6 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
/* padding: 20px; */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,27 +2,38 @@
|
|||||||
<div class="kpi-container">
|
<div class="kpi-container">
|
||||||
<dv-border-box-10 class="kpi-card">
|
<dv-border-box-10 class="kpi-card">
|
||||||
<div class="chart-title">今日告警</div>
|
<div class="chart-title">今日告警</div>
|
||||||
<div class="kpi-value">6549.69<span class="monitor-unit">个</span></div>
|
<div class="kpi-value">{{ formatNumber(kpiData.alert_events) }}<span class="monitor-unit">个</span></div>
|
||||||
</dv-border-box-10>
|
</dv-border-box-10>
|
||||||
<dv-border-box-10 class="kpi-card online-monitor">
|
<dv-border-box-10 class="kpi-card online-monitor">
|
||||||
<div class="chart-title">在线监控数</div>
|
<div class="chart-title">在线监控数</div>
|
||||||
<div class="kpi-value">
|
<div class="kpi-value">
|
||||||
<dv-border-box-12 class="monitor-box">
|
<dv-border-box-12 class="monitor-box">
|
||||||
<div class="monitor-title">当前在线监控</div>
|
<div class="monitor-title">当前在线监控</div>
|
||||||
<div class="monitor-value">13,365<span class="monitor-unit">台</span></div>
|
<div class="monitor-value">{{ formatNumber(kpiData.online_devices) }}<span class="monitor-unit">台</span></div>
|
||||||
</dv-border-box-12>
|
</dv-border-box-12>
|
||||||
</div>
|
</div>
|
||||||
</dv-border-box-10>
|
</dv-border-box-10>
|
||||||
<dv-border-box-10 class="kpi-card algorithm-card">
|
<dv-border-box-10 class="kpi-card algorithm-card">
|
||||||
<div class="chart-title">算法执行</div>
|
<div class="chart-title">算法执行</div>
|
||||||
<div class="algorithm-content">
|
<div class="algorithm-content">
|
||||||
<div class="algorithm-subtitle">累计执行次数</div>
|
<div class="algorithm-subtitle">活跃算法数量</div>
|
||||||
<div class="flip-counter">
|
<div class="flip-counter">
|
||||||
<div class="flip-digit" v-for="(digit, index) in digits" :key="index">
|
<template v-if="algorithmDigits.length === 1 && algorithmDigits[0].current === 0">
|
||||||
<div class="digit-scroll" :style="{ transform: `translateY(${-digit.current * 10}%)` }">
|
<!-- 单独处理0的情况 -->
|
||||||
<span v-for="n in 10" :key="n">{{ n }}</span>
|
<div class="flip-digit">
|
||||||
|
<div class="digit-scroll" style="transform: translateY(0%)">
|
||||||
|
<span>0</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- 正常数字显示 -->
|
||||||
|
<div class="flip-digit" v-for="(digit, index) in algorithmDigits" :key="index">
|
||||||
|
<div class="digit-scroll" :style="{ transform: `translateY(${-digit.current * 10}%)` }">
|
||||||
|
<span v-for="n in 10" :key="n">{{ n }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dv-border-box-10>
|
</dv-border-box-10>
|
||||||
@ -31,56 +42,82 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import API from '@/api/index'
|
||||||
|
|
||||||
// 初始显示22415
|
// 定义响应式数据
|
||||||
const digits = ref([
|
const kpiData = ref({
|
||||||
{ current: 2, target: 2 },
|
total_devices: 0,
|
||||||
{ current: 2, target: 2 },
|
online_devices: 0,
|
||||||
{ current: 4, target: 4 },
|
total_algorithms: 0,
|
||||||
{ current: 1, target: 6 },
|
active_algorithms: 0,
|
||||||
{ current: 5, target: 4 },
|
total_events: 0,
|
||||||
{ current: 5, target: 4 }
|
today_events: 0,
|
||||||
])
|
alert_events: 0,
|
||||||
|
resolved_events: 0
|
||||||
|
})
|
||||||
|
|
||||||
const animateDigits = () => {
|
// 初始化数字翻牌器
|
||||||
// 3秒后开始变化到22464
|
const algorithmDigits = ref([])
|
||||||
setTimeout(() => {
|
|
||||||
digits.value.forEach((digit, index) => {
|
// 格式化数字显示(添加千位分隔符)
|
||||||
setTimeout(() => {
|
const formatNumber = (num) => {
|
||||||
let steps = Math.abs(digit.target - digit.current)
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||||
if (steps > 5) steps = 10 - steps
|
}
|
||||||
|
|
||||||
const duration = 1000
|
// 将数字转换为数字数组,用于翻牌器
|
||||||
const startTime = Date.now()
|
const numberToDigits = (number) => {
|
||||||
|
if (number === 0) {
|
||||||
const animate = () => {
|
return [{ current: 0, target: 0 }]
|
||||||
const elapsed = Date.now() - startTime
|
}
|
||||||
const progress = Math.min(elapsed / duration, 1)
|
|
||||||
|
const numStr = number.toString()
|
||||||
digit.current = Math.floor(digit.current + progress * (digit.target - digit.current))
|
return numStr.split('').map(d => ({
|
||||||
if (digit.current > 9) digit.current = 0
|
current: parseInt(d),
|
||||||
if (digit.current < 0) digit.current = 9
|
target: parseInt(d)
|
||||||
|
}))
|
||||||
if (progress < 1) {
|
}
|
||||||
requestAnimationFrame(animate)
|
|
||||||
} else {
|
// 更新数字翻牌器
|
||||||
digit.current = digit.target
|
const updateDigits = (targetNumber) => {
|
||||||
}
|
algorithmDigits.value = numberToDigits(targetNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
animate()
|
// 获取KPI数据
|
||||||
}, index * 300)
|
const fetchKpiData = async () => {
|
||||||
})
|
try {
|
||||||
}, 1000)
|
const response = await API.getKpi()
|
||||||
|
kpiData.value = response
|
||||||
|
|
||||||
|
// 特殊处理0的情况
|
||||||
|
if (kpiData.value.active_algorithms === 0) {
|
||||||
|
algorithmDigits.value = [{ current: 0, target: 0}]
|
||||||
|
} else {
|
||||||
|
updateDigits(kpiData.value.active_algorithms)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取KPI数据失败:', error)
|
||||||
|
// 设置默认值
|
||||||
|
kpiData.value = {
|
||||||
|
total_devices: 0,
|
||||||
|
online_devices: 0,
|
||||||
|
total_algorithms: 0,
|
||||||
|
active_algorithms: 0,
|
||||||
|
total_events: 0,
|
||||||
|
today_events: 0,
|
||||||
|
alert_events: 0,
|
||||||
|
resolved_events: 0
|
||||||
|
}
|
||||||
|
algorithmDigits.value = [{ current: 0, target: 0 }]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
animateDigits()
|
fetchKpiData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 基础样式 */
|
|
||||||
.kpi-container {
|
.kpi-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -94,10 +131,6 @@ onMounted(() => {
|
|||||||
width: 91%;
|
width: 91%;
|
||||||
height: 25vh;
|
height: 25vh;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
/* display: flex; */
|
|
||||||
/* flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
@ -208,6 +241,7 @@ onMounted(() => {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 1000%; /* 10个数字的高度 */
|
||||||
transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
|
transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,4 +254,10 @@ onMounted(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 单独为0设置的样式 */
|
||||||
|
.flip-digit .digit-scroll[style*="translateY(0%)"] span {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
// src/utils/http.js
|
// src/utils/http.js
|
||||||
import axios from '../config/service' // 基础配置
|
import axios from './service' // 基础配置
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 高级请求封装
|
* 高级请求封装
|
||||||
|
@ -2,7 +2,7 @@ import axios from 'axios';
|
|||||||
|
|
||||||
// 创建 Axios 实例
|
// 创建 Axios 实例
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 从环境变量读取
|
baseURL: '/api', // 从环境变量读取
|
||||||
timeout: 10000, // 请求超时时间
|
timeout: 10000, // 请求超时时间
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,13 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0', // 监听所有网络接口
|
host: '0.0.0.0', // 监听所有网络接口
|
||||||
port: 5173, // 默认端口
|
port: 5173, // 默认端口
|
||||||
strictPort: true // 如果端口被占用则退出
|
strictPort: true, // 如果端口被占用则退出
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://36.103.203.89:6789',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user