fix: 首页数据大屏联调接口

This commit is contained in:
liuzhiyuan 2025-08-06 15:56:07 +08:00
parent b71a9b398d
commit 859dbbb1c7
11 changed files with 616 additions and 280 deletions

View File

@ -11,6 +11,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@kjgl77/datav-vue3": "^1.7.4",
"axios": "^1.11.0",
"echarts": "^5.6.0",
"echarts-liquidfill": "^3.1.0",
"element-plus": "^2.10.4",

View File

@ -23,12 +23,17 @@
</div>
<div class="center-section">
<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 class="right-section">
<dv-border-box-1>
<EventHotSpots />
<AlgorithmStats />
<EventHotSpots :locations="cameraStats.by_location" />
<AlgorithmStats :algorithm-types="algorithmStats.by_type" />
</dv-border-box-1>
</div>
</div>
@ -80,13 +85,57 @@ import AlgorithmCenter from './components/algorithm/AlgorithmCenter.vue'
import AlarmManagement from './components/alarm/AlarmManagement.vue'
import EventCenter from './components/event/EventCenter.vue'
import DeviceManagement from './components/device/DeviceManagement.vue'
import API from '@/api/index'
const activeTab = ref('overview')
const dashboardContainer = ref(null)
const isFullscreen = 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({
id: 1,
name: '港口区主监控',
@ -94,91 +143,95 @@ const currentMonitor = ref({
status: 'online',
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,
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 getMonitorsList = async () => {
const list = await API.getMonitorsList()
monitors.value = list.monitors || []
}
//
const handleTabChange = (tab) => {
@ -217,6 +270,9 @@ const handleFullscreenChange = () => {
//
onMounted(() => {
getMonitorsList()
fetchCameraStats()
fetchAlgorithmStats()
document.addEventListener('fullscreenchange', handleFullscreenChange)
})

View File

@ -1,11 +1,89 @@
import http from '../utils/http'
export default {
login(username, password) {
return http.post('/user/login', { username, password })
getKpi() {
return http.get('/dashboard/kpi')
},
getInfo() {
return http.get('/user/info')
}
getAlarmTrend() {
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)
})
}
}

View File

@ -10,12 +10,57 @@
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import API from '@/api/index'
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(() => {
const myChart = echarts.init(chart.value)
myChart = echarts.init(chart.value)
//
const option = {
backgroundColor: 'transparent',
tooltip: {
@ -44,7 +89,7 @@ onMounted(() => {
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月'],
data: [], // API
axisLine: { lineStyle: { color: 'rgba(124, 208, 255, 0.5)' } },
axisLabel: {
color: 'rgba(124, 208, 255, 0.8)',
@ -55,8 +100,8 @@ onMounted(() => {
yAxis: {
type: 'value',
min: 0,
max: 550,
interval: 50,
max: 60, //
interval: 10,
axisLine: {
lineStyle: { color: 'rgba(124, 208, 255, 0.5)' }
},
@ -84,7 +129,7 @@ onMounted(() => {
borderWidth: 1,
borderColor: '#fff'
},
data: [300, 100, 400, 250, 200]
data: [] // API
},
{
name: 'P1告警',
@ -98,7 +143,7 @@ onMounted(() => {
borderWidth: 1,
borderColor: '#fff'
},
data: [300, 100, 400, 250, 200]
data: [] // API
},
{
name: 'P2告警',
@ -112,7 +157,7 @@ onMounted(() => {
borderWidth: 1,
borderColor: '#fff'
},
data: [350, 150, 550, 50, 110]
data: [] // API
},
{
name: 'P3告警',
@ -126,13 +171,16 @@ onMounted(() => {
borderWidth: 1,
borderColor: '#fff'
},
data: [200, 300, 400, 100, 50]
data: [] // API
}
]
}
myChart.setOption(option)
//
fetchAlarmTrend()
window.addEventListener('resize', function() {
myChart.resize()
})
@ -142,17 +190,16 @@ onMounted(() => {
<style scoped>
.alert-trend-container {
width: 100%;
height: 50vh; /* 增加高度 */
height: 50vh;
padding: 15px 10px;
box-sizing: border-box;
margin-top: 50px; /* 增加上方间距 */
margin-top: 50px;
}
.border-box {
width: 100%;
height: 100%;
border-radius: 4px;
/* padding: 20px; */
box-sizing: border-box;
}

View File

@ -8,12 +8,75 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
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 = {
backgroundColor: 'transparent',
tooltip: {
@ -24,10 +87,20 @@ onMounted(() => {
textStyle: {
color: '#fff',
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: {
data: ['目标识别算法', '行为识别算法'],
data: props.algorithmTypes.map(item => item.type),
bottom: 10,
textStyle: {
color: '#fff',
@ -38,13 +111,7 @@ onMounted(() => {
itemGap: 20
},
radar: {
indicator: [
{ name: '命中率', max: 100 },
{ name: '误报率', max: 100 },
{ name: '触发器', max: 100 },
{ name: '处理时延', max: 100 },
{ name: '稳定性', max: 100 }
],
indicator: indicators,
radius: '65%',
axisName: {
color: 'rgba(124, 208, 255, 0.8)',
@ -69,60 +136,32 @@ onMounted(() => {
},
series: [{
type: 'radar',
data: [
{
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)'
}
}
]
data: seriesData
}]
}
const chartInstance = echarts.init(radarChart.value)
chartInstance.setOption(radarOption)
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance.resize()
chartInstance && chartInstance.resize()
})
})
watch(() => props.algorithmTypes, () => {
if (chartInstance) {
initChart()
}
}, { deep: true })
</script>
<style scoped>
.radar-chart-container {
width: 100%;
height: 40vh; /* 高度占一半 */
height: 40vh;
padding: 15px 30px;
box-sizing: border-box;
}
@ -131,7 +170,6 @@ onMounted(() => {
width: 100%;
height: 100%;
border-radius: 4px;
/* padding: 20px; */
box-sizing: border-box;
}

View File

@ -5,12 +5,12 @@
<div class="gauge-container">
<div class="gauge">
<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>
<div class="gauge">
<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>
</div>
@ -19,59 +19,115 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
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 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(() => {
//
const createLiquidOption = (color) => ({
series: [{
type: 'liquidFill',
radius: '70%',
center: ['50%', '50%'],
data: [0.338, 0.48], //
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 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()
})
updateCharts()
const resizeHandler = () => {
[onlineRateChart.value, coverageChart.value].forEach(chart => {
if (chart) {
const instance = echarts.getInstanceByDom(chart)
instance && instance.resize()
}
})
}
window.addEventListener('resize', resizeHandler)
return () => {
window.removeEventListener('resize', resizeHandler)
[onlineRateChart.value, coverageChart.value].forEach(chart => {
if (chart) {
const instance = echarts.getInstanceByDom(chart)
instance && instance.dispose()
}
})
}
})
</script>
<style scoped>
/* 样式保持不变 */
.stats-container {
height: 30vh;
box-sizing: border-box;
padding: 15px 10px;

View File

@ -8,12 +8,24 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
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 = {
backgroundColor: 'transparent',
tooltip: {
@ -25,9 +37,9 @@ onMounted(() => {
textStyle: { color: '#fff' }
},
legend: {
data: ['检测数据1', '检测数据2'],
bottom: 10, // 10px
left: 'center', //
data: ['摄像头总数', '在线数量'],
bottom: 10,
left: 'center',
textStyle: {
color: '#fff',
fontSize: 12
@ -39,15 +51,15 @@ onMounted(() => {
grid: {
left: '2%',
right: '4%',
bottom: '15%', //
bottom: '15%',
top: '5%',
containLabel: true
},
xAxis: {
type: 'value',
min: 0,
max: 250,
interval: 50,
max: Math.max(...props.locations.map(l => l.total)) + 10,
interval: Math.ceil(Math.max(...props.locations.map(l => l.total)) / 5),
axisLine: {
lineStyle: {
color: 'rgba(124, 208, 255, 0.5)'
@ -66,13 +78,7 @@ onMounted(() => {
},
yAxis: {
type: 'category',
data: [
'办公走廊/楼道区',
'会议室区域',
'值班岗亭区',
'边检通道区',
'港口泊位区'
],
data: props.locations.map(l => l.location),
axisLine: {
lineStyle: {
color: 'rgba(124, 208, 255, 0.5)'
@ -88,7 +94,7 @@ onMounted(() => {
},
series: [
{
name: '检测数据1',
name: '摄像头总数',
type: 'bar',
barWidth: 15,
label: {
@ -102,10 +108,10 @@ onMounted(() => {
color: '#FFA500',
borderRadius: [0, 4, 4, 0]
},
data: [120, 150, 200, 100, 150]
data: props.locations.map(l => l.total)
},
{
name: '检测数据2',
name: '在线数量',
type: 'bar',
barWidth: 15,
label: {
@ -119,24 +125,33 @@ onMounted(() => {
color: '#00FFC0',
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)
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance.resize()
chartInstance && chartInstance.resize()
})
})
watch(() => props.locations, () => {
if (chartInstance) {
initChart()
}
}, { deep: true })
</script>
<style scoped>
.half-height-chart-container {
width: 100%;
height: 40vh; /* 关键修改高度设为视口高度的50% */
height: 40vh;
padding: 15px 30px;
box-sizing: border-box;
margin-top: 20px;
@ -146,7 +161,6 @@ onMounted(() => {
width: 100%;
height: 100%;
border-radius: 4px;
/* padding: 20px; */
box-sizing: border-box;
}

View File

@ -2,27 +2,38 @@
<div class="kpi-container">
<dv-border-box-10 class="kpi-card">
<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 class="kpi-card online-monitor">
<div class="chart-title">在线监控数</div>
<div class="kpi-value">
<dv-border-box-12 class="monitor-box">
<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>
</div>
</dv-border-box-10>
<dv-border-box-10 class="kpi-card algorithm-card">
<div class="chart-title">算法执行</div>
<div class="algorithm-content">
<div class="algorithm-subtitle">累计执行次数</div>
<div class="algorithm-subtitle">活跃算法数量</div>
<div class="flip-counter">
<div class="flip-digit" v-for="(digit, index) in digits" :key="index">
<div class="digit-scroll" :style="{ transform: `translateY(${-digit.current * 10}%)` }">
<span v-for="n in 10" :key="n">{{ n }}</span>
<template v-if="algorithmDigits.length === 1 && algorithmDigits[0].current === 0">
<!-- 单独处理0的情况 -->
<div class="flip-digit">
<div class="digit-scroll" style="transform: translateY(0%)">
<span>0</span>
</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>
</dv-border-box-10>
@ -31,56 +42,82 @@
<script setup>
import { ref, onMounted } from 'vue'
import API from '@/api/index'
// 22415
const digits = ref([
{ current: 2, target: 2 },
{ current: 2, target: 2 },
{ current: 4, target: 4 },
{ current: 1, target: 6 },
{ current: 5, target: 4 },
{ current: 5, target: 4 }
])
//
const kpiData = ref({
total_devices: 0,
online_devices: 0,
total_algorithms: 0,
active_algorithms: 0,
total_events: 0,
today_events: 0,
alert_events: 0,
resolved_events: 0
})
const animateDigits = () => {
// 322464
setTimeout(() => {
digits.value.forEach((digit, index) => {
setTimeout(() => {
let steps = Math.abs(digit.target - digit.current)
if (steps > 5) steps = 10 - steps
const duration = 1000
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
digit.current = Math.floor(digit.current + progress * (digit.target - digit.current))
if (digit.current > 9) digit.current = 0
if (digit.current < 0) digit.current = 9
if (progress < 1) {
requestAnimationFrame(animate)
} else {
digit.current = digit.target
}
}
animate()
}, index * 300)
})
}, 1000)
//
const algorithmDigits = ref([])
//
const formatNumber = (num) => {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}
//
const numberToDigits = (number) => {
if (number === 0) {
return [{ current: 0, target: 0 }]
}
const numStr = number.toString()
return numStr.split('').map(d => ({
current: parseInt(d),
target: parseInt(d)
}))
}
//
const updateDigits = (targetNumber) => {
algorithmDigits.value = numberToDigits(targetNumber)
}
// KPI
const fetchKpiData = async () => {
try {
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(() => {
animateDigits()
fetchKpiData()
})
</script>
<style scoped>
/* 基础样式 */
.kpi-container {
display: flex;
flex-direction: column;
@ -94,10 +131,6 @@ onMounted(() => {
width: 91%;
height: 25vh;
margin: 10px 0;
/* display: flex; */
/* flex-direction: column;
justify-content: center;
align-items: center; */
}
.chart-title {
@ -208,6 +241,7 @@ onMounted(() => {
top: 0;
left: 0;
width: 100%;
height: 1000%; /* 10个数字的高度 */
transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
}
@ -220,4 +254,10 @@ onMounted(() => {
font-weight: bold;
color: white;
}
/* 单独为0设置的样式 */
.flip-digit .digit-scroll[style*="translateY(0%)"] span {
height: 50px;
line-height: 50px;
}
</style>

View File

@ -1,5 +1,5 @@
// src/utils/http.js
import axios from '../config/service' // 基础配置
import axios from './service' // 基础配置
/**
* 高级请求封装

View File

@ -2,7 +2,7 @@ import axios from 'axios';
// 创建 Axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 从环境变量读取
baseURL: '/api', // 从环境变量读取
timeout: 10000, // 请求超时时间
});

View File

@ -7,7 +7,13 @@ export default defineConfig({
server: {
host: '0.0.0.0', // 监听所有网络接口
port: 5173, // 默认端口
strictPort: true // 如果端口被占用则退出
strictPort: true, // 如果端口被占用则退出
proxy: {
'/api': {
target: 'http://36.103.203.89:6789',
changeOrigin: true,
}
}
},
resolve: {
alias: {