修改了可视化大屏的动画BUg
This commit is contained in:
parent
0d5d1e6072
commit
a8b6fb4b8b
2
.env
2
.env
@ -22,4 +22,4 @@ VITE_BUILD_BROTLI=false
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 后端 API 的基础 URL
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_API_BASE_URL=http://127.0.0.1:3000
|
@ -2,7 +2,7 @@
|
||||
VITE_NODE_ENV=development
|
||||
|
||||
# API 基础路径
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_API_BASE_URL=http://127.0.0.1:3000
|
||||
|
||||
# 项目基础路径
|
||||
VITE_BASE_URL=/
|
||||
|
@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -371,13 +371,13 @@ const router = createRouter({
|
||||
children: [
|
||||
{
|
||||
path: 'projects',
|
||||
name: 'Projects',
|
||||
name: 'Projects',
|
||||
component: () => import('@/views/about/projects/index.vue'),
|
||||
meta: { title: '简介' }
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
path: 'needToKnow',
|
||||
name: 'NeedToKnow',
|
||||
name: 'NeedToKnow',
|
||||
component: () => import('@/views/about/needToKnow/index.vue'),
|
||||
meta: { title: '游园需知' }
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import flvjs from 'flv.js'
|
||||
|
||||
// 视频流服务器配置
|
||||
const VIDEO_SERVER = {
|
||||
url: 'http://localhost:8799/live',
|
||||
url: 'http://127.0.0.1:8799/live',
|
||||
type: 'flv'
|
||||
}
|
||||
|
||||
|
@ -253,19 +253,19 @@ const handleKeyPress = (event) => {
|
||||
let baseValue;
|
||||
switch (key) {
|
||||
case "h":
|
||||
// 海水基准值:1185,浮动范围±15
|
||||
// 海水基准值:1185,浮动范围±3
|
||||
baseValue = 1185;
|
||||
randomValue = baseValue + (Math.random() * 30 - 15);
|
||||
randomValue = baseValue + (Math.random() * 6 - 3);
|
||||
break;
|
||||
case "c":
|
||||
// 茶水基准值:224,浮动范围±8
|
||||
// 茶水基准值:224,浮动范围±2
|
||||
baseValue = 224;
|
||||
randomValue = baseValue + (Math.random() * 16 - 8);
|
||||
randomValue = baseValue + (Math.random() * 4 - 2);
|
||||
break;
|
||||
case "f":
|
||||
// 芬达基准值:470,浮动范围±10
|
||||
// 芬达基准值:470,浮动范围±2
|
||||
baseValue = 470;
|
||||
randomValue = baseValue + (Math.random() * 20 - 10);
|
||||
randomValue = baseValue + (Math.random() * 4 - 2);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ const imageUrl = computed(() => {
|
||||
if (url.startsWith('data:')) return url
|
||||
if (url.startsWith('http')) return url
|
||||
// 使用后端服务器URL
|
||||
return `http://localhost:3000${url}`
|
||||
return `http://127.0.0.1:3000${url}`
|
||||
})
|
||||
|
||||
// 获取上传请求头
|
||||
@ -519,7 +519,7 @@ onMounted(() => {
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
action="http://localhost:3000/api/admin/projects/upload"
|
||||
action="http://127.0.0.1:3000/api/admin/projects/upload"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
|
@ -92,7 +92,7 @@ const getList = async () => {
|
||||
cover_image: item.cover_image
|
||||
? (item.cover_image.startsWith('http')
|
||||
? item.cover_image
|
||||
: `http://localhost:3000${item.cover_image}`)
|
||||
: `http://127.0.0.1:3000${item.cover_image}`)
|
||||
: '',
|
||||
created_at: formatDateTime(item.created_at),
|
||||
updated_at: formatDateTime(item.updated_at)
|
||||
@ -222,7 +222,7 @@ const handleEdit = (row) => {
|
||||
category: row.category,
|
||||
content: row.content,
|
||||
cover_image: row.cover_image && !row.cover_image.startsWith('http')
|
||||
? `http://localhost:3000${row.cover_image}`
|
||||
? `http://127.0.0.1:3000${row.cover_image}`
|
||||
: row.cover_image,
|
||||
tags: row.tags || [],
|
||||
status: row.status,
|
||||
@ -243,7 +243,7 @@ const handleSubmit = async () => {
|
||||
title: form.value.title.trim(),
|
||||
category: form.value.category,
|
||||
content: form.value.content.trim(),
|
||||
cover_image: form.value.cover_image.replace('http://localhost:3000', ''),
|
||||
cover_image: form.value.cover_image.replace('http://127.0.0.1:3000', ''),
|
||||
tags: form.value.tags,
|
||||
status: Number(form.value.status)
|
||||
}
|
||||
@ -339,7 +339,7 @@ const handleUploadSuccess = (response) => {
|
||||
// 确保使用完整的URL
|
||||
form.value.cover_image = imageUrl.startsWith('http')
|
||||
? imageUrl
|
||||
: `http://localhost:3000${imageUrl}`
|
||||
: `http://127.0.0.1:3000${imageUrl}`
|
||||
ElMessage.success('图片上传成功')
|
||||
} else {
|
||||
form.value.cover_image = ''
|
||||
@ -547,7 +547,7 @@ onMounted(() => {
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="image-upload"
|
||||
action="http://localhost:3000/api/education/knowledge/upload"
|
||||
action="http://127.0.0.1:3000/api/education/knowledge/upload"
|
||||
:headers="uploadHeaders"
|
||||
name="cover_image"
|
||||
:show-file-list="false"
|
||||
|
@ -204,7 +204,7 @@ const getFileNameFromUrl = (url) => {
|
||||
}
|
||||
|
||||
// 如果包含域名但不是以http开头
|
||||
if (cleanUrl.includes('localhost:3000')) {
|
||||
if (cleanUrl.includes('127.0.0.1:3000')) {
|
||||
const matches = cleanUrl.match(/([^/]+)$/)
|
||||
return matches ? matches[1] : cleanUrl
|
||||
}
|
||||
@ -223,14 +223,14 @@ const processImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/images\/[^/]+$/)) {
|
||||
if (url.match(/^http:\/\/127.0.0.1:3000\/uploads\/courses\/images\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
return `http://127.0.0.1:3000/uploads/courses/images/${fileName}`
|
||||
}
|
||||
|
||||
// 处理视频URL
|
||||
@ -238,14 +238,14 @@ const processVideoUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/videos\/[^/]+$/)) {
|
||||
if (url.match(/^http:\/\/127.0.0.1:3000\/uploads\/courses\/videos\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/videos/${fileName}`
|
||||
return `http://127.0.0.1:3000/uploads/courses/videos/${fileName}`
|
||||
}
|
||||
|
||||
// 图片上传相关
|
||||
@ -260,7 +260,7 @@ const handleImageSuccess = (response) => {
|
||||
console.log('上传返回的图片信息:', response.data)
|
||||
// 保存图片的所有相关信息
|
||||
const fileName = response.data.url.split('/').pop()
|
||||
const imageUrl = `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
const imageUrl = `http://127.0.0.1:3000/uploads/courses/images/${fileName}`
|
||||
|
||||
// 设置表单中的图片相关字段
|
||||
form.value = {
|
||||
@ -336,7 +336,7 @@ const handleEdit = (row) => {
|
||||
}
|
||||
|
||||
// 如果包含域名但不是以http开头
|
||||
if (cleanUrl.includes('localhost:3000')) {
|
||||
if (cleanUrl.includes('127.0.0.1:3000')) {
|
||||
const matches = cleanUrl.match(/([^/]+)$/)
|
||||
return matches ? matches[1] : cleanUrl
|
||||
}
|
||||
@ -355,14 +355,14 @@ const handleEdit = (row) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/images\/[^/]+$/)) {
|
||||
if (url.match(/^http:\/\/127.0.0.1:3000\/uploads\/courses\/images\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
return `http://127.0.0.1:3000/uploads/courses/images/${fileName}`
|
||||
}
|
||||
|
||||
// 处理视频URL
|
||||
@ -370,14 +370,14 @@ const handleEdit = (row) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/videos\/[^/]+$/)) {
|
||||
if (url.match(/^http:\/\/127.0.0.1:3000\/uploads\/courses\/videos\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/videos/${fileName}`
|
||||
return `http://127.0.0.1:3000/uploads/courses/videos/${fileName}`
|
||||
}
|
||||
|
||||
// 设置表单数据
|
||||
@ -389,9 +389,9 @@ const handleEdit = (row) => {
|
||||
title: row.title,
|
||||
category: row.category,
|
||||
cover_image: fileName, // 只保存文件名
|
||||
cover_image_url: `http://localhost:3000/uploads/courses/images/${fileName}`, // 完整URL用于预览
|
||||
cover_image_url: `http://127.0.0.1:3000/uploads/courses/images/${fileName}`, // 完整URL用于预览
|
||||
video: videoFileName, // 只保存文件名
|
||||
video_url: `http://localhost:3000/uploads/courses/videos/${videoFileName}`, // 完整URL用于预览
|
||||
video_url: `http://127.0.0.1:3000/uploads/courses/videos/${videoFileName}`, // 完整URL用于预览
|
||||
video_duration: row.video_duration || 0,
|
||||
video_size: row.video_size || 0,
|
||||
description: row.description || '',
|
||||
@ -535,7 +535,7 @@ const handleImageError = (e) => {
|
||||
// 获取正确的文件名
|
||||
const fileName = getFileNameFromUrl(form.value.cover_image)
|
||||
// 使用新的URL重试
|
||||
img.src = `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
img.src = `http://127.0.0.1:3000/uploads/courses/images/${fileName}`
|
||||
// 增加重试计数
|
||||
img.dataset.retryCount = String(Number(img.dataset.retryCount) + 1)
|
||||
}
|
||||
@ -546,7 +546,7 @@ const handleVideoSuccess = (response) => {
|
||||
console.log('上传返回的视频信息:', response.data)
|
||||
// 保存视频的所有相关信息
|
||||
const fileName = response.data.url.split('/').pop()
|
||||
const videoUrl = `http://localhost:3000/uploads/courses/videos/${fileName}`
|
||||
const videoUrl = `http://127.0.0.1:3000/uploads/courses/videos/${fileName}`
|
||||
|
||||
// 设置表单中的视频相关字段
|
||||
form.value = {
|
||||
@ -781,7 +781,7 @@ onMounted(() => {
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="image-upload"
|
||||
action="http://localhost:3000/api/education/knowledge/upload"
|
||||
action="http://127.0.0.1:3000/api/education/knowledge/upload"
|
||||
:headers="uploadHeaders"
|
||||
name="cover_image"
|
||||
:show-file-list="false"
|
||||
@ -823,7 +823,7 @@ onMounted(() => {
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="video-upload"
|
||||
action="http://localhost:3000/api/education/courses/upload/video"
|
||||
action="http://127.0.0.1:3000/api/education/courses/upload/video"
|
||||
:headers="uploadHeaders"
|
||||
name="video"
|
||||
:show-file-list="false"
|
||||
|
@ -44,6 +44,7 @@ const categoryChartRef = ref(null);
|
||||
let categoryChart = null;
|
||||
const protectionChartRef = ref(null);
|
||||
let protectionChart = null;
|
||||
const trendChartRef = ref(null);
|
||||
|
||||
// 统计卡片数据
|
||||
const statsCards = ref([
|
||||
@ -252,7 +253,6 @@ const initProtectionChart = () => {
|
||||
protectionChart = echarts.init(protectionChartRef.value);
|
||||
const option = {
|
||||
title: {
|
||||
text: "保护等级统计",
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
@ -361,7 +361,6 @@ const initTrendChart = () => {
|
||||
const myChart = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
text: "近7天监测数据趋势",
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
@ -412,6 +411,14 @@ const initTrendChart = () => {
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
max: 50,
|
||||
interval: 10,
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "#EBEEF5",
|
||||
@ -450,7 +457,7 @@ const initTrendChart = () => {
|
||||
shadowBlur: 10,
|
||||
},
|
||||
},
|
||||
data: [120, 132, 101, 134, 90, 230, 210],
|
||||
data: [10, 12, 15, 13, 18, 15, 20],
|
||||
},
|
||||
{
|
||||
name: "监测数据",
|
||||
@ -479,14 +486,12 @@ const initTrendChart = () => {
|
||||
shadowBlur: 10,
|
||||
},
|
||||
},
|
||||
data: [220, 182, 191, 234, 290, 330, 310],
|
||||
data: [15, 18, 12, 20, 15, 18, 16],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
myChart.setOption(option);
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener("resize", () => {
|
||||
myChart.resize();
|
||||
});
|
||||
@ -631,10 +636,10 @@ const fetchStatisticsData = async () => {
|
||||
onMounted(() => {
|
||||
initData();
|
||||
startAutoRefresh();
|
||||
initTrendChart();
|
||||
initDistributionChart();
|
||||
initCategoryChart();
|
||||
initProtectionChart();
|
||||
initTrendChart();
|
||||
initDistributionChart();
|
||||
window.addEventListener("resize", () => {
|
||||
categoryChart?.resize();
|
||||
protectionChart?.resize();
|
||||
@ -818,67 +823,6 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:deep(.el-timeline-item__node) {
|
||||
background-color: transparent;
|
||||
border: 2px solid v.$primary-color;
|
||||
}
|
||||
|
||||
:deep(.el-timeline-item__tail) {
|
||||
border-left: 2px solid #e4e7ed;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: v.$text-primary;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: v.$primary-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@ -887,10 +831,13 @@ onUnmounted(() => {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-item {
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: v.$text-primary;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,12 @@ const isBuffering = ref(false) // 添加缓冲状态标志
|
||||
let chart = null
|
||||
let ws = null
|
||||
|
||||
// 添加 WebSocket 状态管理
|
||||
const wsConnected = ref(false)
|
||||
let wsRetryCount = 0
|
||||
const MAX_RETRY_COUNT = 3
|
||||
let wsRetryTimer = null
|
||||
|
||||
// 获取图表数据
|
||||
const getChartData = async () => {
|
||||
try {
|
||||
@ -198,49 +204,147 @@ const showDeviceStatusNotification = (status) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
// 修改 WebSocket 初始化和重连逻辑
|
||||
const initWebSocket = () => {
|
||||
ws = new WebSocket('ws://192.168.1.158:6894')
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket连接成功')
|
||||
if (wsRetryCount >= MAX_RETRY_COUNT) {
|
||||
console.warn('WebSocket重试次数超过限制,切换到HTTP轮询模式')
|
||||
startPolling()
|
||||
return
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'device_status') {
|
||||
// 如果 drone.value 不存在,或者设备编码匹配,就更新状态
|
||||
if (!drone.value || (drone.value && drone.value.code === data.device_code)) {
|
||||
// 直接更新设备状态
|
||||
if (drone.value) {
|
||||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||||
if (drone.value.status !== newStatus) {
|
||||
try {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
|
||||
// 修改为本地地址
|
||||
ws = new WebSocket('ws://127.0.0.1:6894')
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket连接成功')
|
||||
wsConnected.value = true
|
||||
wsRetryCount = 0 // 重置重试次数
|
||||
if (wsRetryTimer) {
|
||||
clearTimeout(wsRetryTimer)
|
||||
wsRetryTimer = null
|
||||
}
|
||||
|
||||
// WebSocket连接成功后立即查询设备状态
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'query_status',
|
||||
device_code: 'BC13292E5A49914F4D62B9F356E39F56'
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'device_status') {
|
||||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||||
const oldStatus = drone.value?.status
|
||||
|
||||
// 只在状态真实发生变化时更新
|
||||
if (!drone.value || oldStatus !== newStatus) {
|
||||
// 立即更新设备状态
|
||||
if (!drone.value) {
|
||||
drone.value = {
|
||||
id: data.device_id,
|
||||
name: data.device_name || '无人机',
|
||||
code: data.device_code,
|
||||
status: newStatus,
|
||||
battery: data.battery_level || 0,
|
||||
signal: data.signal_strength || 0
|
||||
}
|
||||
} else {
|
||||
drone.value.status = newStatus
|
||||
}
|
||||
|
||||
// 显示状态变化通知
|
||||
showDeviceStatusNotification(newStatus)
|
||||
|
||||
// 根据新状态初始化视频或图表
|
||||
nextTick(async () => {
|
||||
if (newStatus === 'online') {
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
await initVideoPlayer()
|
||||
} else {
|
||||
if (flvPlayer.value) {
|
||||
destroyFlvPlayer(flvPlayer.value)
|
||||
flvPlayer.value = null
|
||||
}
|
||||
getChartData()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 获取完整的设备信息
|
||||
getDroneInfo()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket消息处理错误:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket消息处理错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error)
|
||||
}
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error)
|
||||
wsConnected.value = false
|
||||
wsRetryCount++
|
||||
|
||||
ws.onclose = () => {
|
||||
// 尝试重新连接
|
||||
setTimeout(initWebSocket, 5000)
|
||||
// 如果重试次数超过限制,切换到HTTP轮询
|
||||
if (wsRetryCount >= MAX_RETRY_COUNT) {
|
||||
console.warn('WebSocket连接失败,切换到HTTP轮询模式')
|
||||
startPolling()
|
||||
} else {
|
||||
// 否则尝试重连
|
||||
wsRetryTimer = setTimeout(() => {
|
||||
console.log(`尝试WebSocket重连 (${wsRetryCount}/${MAX_RETRY_COUNT})...`)
|
||||
initWebSocket()
|
||||
}, 5000 * Math.min(wsRetryCount, 3)) // 重试间隔随次数增加但最多15秒
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket连接关闭')
|
||||
wsConnected.value = false
|
||||
|
||||
// 只有在非主动关闭的情况下才重连
|
||||
if (!ws._closeInitiated) {
|
||||
wsRetryTimer = setTimeout(() => {
|
||||
initWebSocket()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket初始化失败:', error)
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取无人机信息
|
||||
// 添加HTTP轮询模式
|
||||
let pollingTimer = null
|
||||
const startPolling = () => {
|
||||
// 清理已有的定时器
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer)
|
||||
}
|
||||
|
||||
// 立即执行一次
|
||||
getDroneInfo()
|
||||
|
||||
// 设置轮询间隔
|
||||
pollingTimer = setInterval(() => {
|
||||
getDroneInfo()
|
||||
}, 10000) // 10秒轮询一次
|
||||
}
|
||||
|
||||
// 修改获取设备信息的逻辑
|
||||
const getDroneInfo = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await getDeviceList({
|
||||
page: 1,
|
||||
@ -249,18 +353,14 @@ const getDroneInfo = async () => {
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
const targetDrone = res.data.list.find(item => {
|
||||
return item.device_code === 'BC13292E5A49914F4D62B9F356E39F56'
|
||||
})
|
||||
const targetDrone = res.data.list.find(item =>
|
||||
item.device_code === 'BC13292E5A49914F4D62B9F356E39F56'
|
||||
)
|
||||
|
||||
if (targetDrone) {
|
||||
// 设备状态判断逻辑:
|
||||
// 1. 检查WebSocket连接状态
|
||||
const wsConnected = ws?.readyState === WebSocket.OPEN
|
||||
// 2. 检查设备状态 - 优先使用 WebSocket 状态
|
||||
// 设备状态判断逻辑
|
||||
const deviceStatus = targetDrone.status?.code || targetDrone.status
|
||||
const deviceOnline = wsConnected && (deviceStatus === 1 || (drone.value?.status === 'online'))
|
||||
// 3. 设置新状态
|
||||
const deviceOnline = deviceStatus === 1
|
||||
const newStatus = deviceOnline ? 'online' : 'offline'
|
||||
|
||||
// 检查状态是否发生变化
|
||||
@ -269,27 +369,29 @@ const getDroneInfo = async () => {
|
||||
showDeviceStatusNotification(newStatus)
|
||||
}
|
||||
|
||||
drone.value = {
|
||||
id: targetDrone.id,
|
||||
name: targetDrone.device_name,
|
||||
code: targetDrone.device_code,
|
||||
status: newStatus,
|
||||
battery: targetDrone.battery_level || 0,
|
||||
signal: targetDrone.signal_strength || 0
|
||||
// 只有在状态发生变化或首次加载时才更新数据
|
||||
if (!drone.value || oldStatus !== newStatus) {
|
||||
drone.value = {
|
||||
id: targetDrone.id,
|
||||
name: targetDrone.device_name,
|
||||
code: targetDrone.device_code,
|
||||
status: newStatus,
|
||||
battery: targetDrone.battery_level || 0,
|
||||
signal: targetDrone.signal_strength || 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drone.value = null
|
||||
getChartData()
|
||||
if (drone.value !== null) {
|
||||
drone.value = null
|
||||
getChartData()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('API响应错误:', res.message)
|
||||
ElMessage.error(res.message || '获取无人机列表失败')
|
||||
getChartData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取无人机列表错误:', error)
|
||||
ElMessage.error('获取无人机列表失败')
|
||||
getChartData()
|
||||
console.error('获取设备信息失败:', error)
|
||||
if (!drone.value) {
|
||||
getChartData()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -299,68 +401,125 @@ const getDroneInfo = async () => {
|
||||
const initVideoPlayer = async () => {
|
||||
videoLoading.value = true // 开始加载时显示loading
|
||||
isPlaying.value = false // 重置播放状态
|
||||
isBuffering.value = false // 重置缓冲状态
|
||||
|
||||
// 销毁之前的播放器
|
||||
if (flvPlayer.value) {
|
||||
destroyFlvPlayer(flvPlayer.value)
|
||||
flvPlayer.value = null
|
||||
}
|
||||
|
||||
// 初始化新的播放器
|
||||
if (videoRef.value && drone.value?.status === 'online') {
|
||||
flvPlayer.value = createFlvPlayer(videoRef.value, drone.value.code)
|
||||
|
||||
try {
|
||||
// 销毁之前的播放器
|
||||
if (flvPlayer.value) {
|
||||
flvPlayer.value.on('loading', () => {
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
}
|
||||
})
|
||||
|
||||
flvPlayer.value.on('error', (errorType, errorDetail) => {
|
||||
console.error('视频播放错误:', errorType, errorDetail)
|
||||
ElMessage.error('视频加载失败,请检查网络连接')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
})
|
||||
|
||||
// 监听视频元素的事件
|
||||
videoRef.value.addEventListener('waiting', () => {
|
||||
// 视频缓冲中
|
||||
isBuffering.value = true
|
||||
// 只有在视频还未开始播放时才显示loading
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
}
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('playing', () => {
|
||||
// 视频开始播放
|
||||
videoLoading.value = false
|
||||
isPlaying.value = true
|
||||
isBuffering.value = false
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('canplay', () => {
|
||||
// 视频可以播放
|
||||
isBuffering.value = false
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('error', () => {
|
||||
console.error('视频播放错误')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
ElMessage.error('视频加载失败,请检查网络连接')
|
||||
})
|
||||
} else {
|
||||
console.error('播放器创建失败')
|
||||
ElMessage.error('视频播放器初始化失败')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
destroyFlvPlayer(flvPlayer.value)
|
||||
flvPlayer.value = null
|
||||
}
|
||||
|
||||
// 等待 DOM 更新
|
||||
await nextTick()
|
||||
|
||||
// 初始化新的播放器
|
||||
if (videoRef.value && drone.value?.status === 'online') {
|
||||
// 重置视频元素
|
||||
const videoElement = videoRef.value
|
||||
videoElement.pause()
|
||||
videoElement.currentTime = 0
|
||||
videoElement.muted = true
|
||||
|
||||
// 移除旧的事件监听器
|
||||
const oldListeners = videoElement._eventListeners || []
|
||||
oldListeners.forEach(({ event, handler }) => {
|
||||
videoElement.removeEventListener(event, handler)
|
||||
})
|
||||
videoElement._eventListeners = []
|
||||
|
||||
// 创建新的播放器
|
||||
flvPlayer.value = createFlvPlayer(videoElement, drone.value.code)
|
||||
|
||||
if (flvPlayer.value) {
|
||||
// 添加 flv.js 事件监听
|
||||
flvPlayer.value.on('loading', () => {
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
isBuffering.value = true
|
||||
}
|
||||
})
|
||||
|
||||
flvPlayer.value.on('error', (errorType, errorDetail) => {
|
||||
console.error('视频播放错误:', errorType, errorDetail)
|
||||
ElMessage.error('视频加载失败,请检查网络连接')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
isBuffering.value = false
|
||||
})
|
||||
|
||||
// 添加视频元素事件监听
|
||||
const addVideoListener = (event, handler) => {
|
||||
videoElement.addEventListener(event, handler)
|
||||
videoElement._eventListeners.push({ event, handler })
|
||||
}
|
||||
|
||||
addVideoListener('loadstart', () => {
|
||||
videoLoading.value = true
|
||||
isBuffering.value = true
|
||||
})
|
||||
|
||||
addVideoListener('waiting', () => {
|
||||
isBuffering.value = true
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
}
|
||||
})
|
||||
|
||||
addVideoListener('playing', () => {
|
||||
videoLoading.value = false
|
||||
isPlaying.value = true
|
||||
isBuffering.value = false
|
||||
})
|
||||
|
||||
addVideoListener('canplay', () => {
|
||||
isBuffering.value = false
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
addVideoListener('error', (e) => {
|
||||
console.error('视频元素错误:', e)
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
isBuffering.value = false
|
||||
ElMessage.error('视频加载失败,请检查网络连接')
|
||||
})
|
||||
|
||||
// 尝试播放视频
|
||||
try {
|
||||
await videoElement.play()
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('视频播放被中断,重试中...')
|
||||
// 短暂延迟后重试播放
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await videoElement.play()
|
||||
} catch (retryError) {
|
||||
console.error('视频重试播放失败:', retryError)
|
||||
ElMessage.error('视频播放失败,请刷新页面重试')
|
||||
}
|
||||
}, 100)
|
||||
} else {
|
||||
console.error('视频播放失败:', error)
|
||||
ElMessage.error('视频播放失败,请检查网络连接')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('播放器创建失败')
|
||||
ElMessage.error('视频播放器初始化失败')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频播放器初始化失败:', error)
|
||||
ElMessage.error('视频播放器初始化失败')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
isBuffering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,11 +527,16 @@ const initVideoPlayer = async () => {
|
||||
let refreshTimer = null
|
||||
const startRefreshTimer = () => {
|
||||
refreshTimer = setInterval(() => {
|
||||
getDroneInfo()
|
||||
// 只在WebSocket连接断开时才通过HTTP轮询获取状态
|
||||
if (ws?.readyState !== WebSocket.OPEN) {
|
||||
getDroneInfo()
|
||||
}
|
||||
|
||||
// 只在设备离线时才刷新图表数据
|
||||
if (drone.value?.status !== 'online') {
|
||||
getChartData()
|
||||
}
|
||||
}, 30000) // 每30秒刷新一次
|
||||
}, 60000) // 改为每60秒刷新一次
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
@ -430,14 +594,25 @@ onUnmounted(() => {
|
||||
document.removeEventListener('click', handleUserInteraction)
|
||||
document.removeEventListener('keydown', handleUserInteraction)
|
||||
|
||||
// 关闭WebSocket连接
|
||||
// 标记WebSocket为主动关闭
|
||||
if (ws) {
|
||||
ws._closeInitiated = true
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
|
||||
// 清理所有定时器
|
||||
if (wsRetryTimer) {
|
||||
clearTimeout(wsRetryTimer)
|
||||
}
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer)
|
||||
}
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
|
||||
// 清理播放器和图表
|
||||
if (flvPlayer.value) {
|
||||
destroyFlvPlayer(flvPlayer.value)
|
||||
}
|
||||
@ -445,6 +620,8 @@ onUnmounted(() => {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
|
||||
wsConnected.value = false
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
@ -18,6 +18,12 @@ let resizeObserver = null
|
||||
// 获取最新数据
|
||||
const fetchLatestData = async () => {
|
||||
try {
|
||||
// 添加加载动画类
|
||||
const chartDom = chartRef.value
|
||||
if (chartDom) {
|
||||
chartDom.classList.add('loading')
|
||||
}
|
||||
|
||||
const res = await getLatestData()
|
||||
if (res.success && res.data) {
|
||||
monitoringData.value = res.data
|
||||
@ -25,6 +31,12 @@ const fetchLatestData = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最新数据失败:', error)
|
||||
} finally {
|
||||
// 移除加载动画类
|
||||
const chartDom = chartRef.value
|
||||
if (chartDom) {
|
||||
chartDom.classList.remove('loading')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,73 +104,120 @@ const initChart = async () => {
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}'
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(0, 24, 65, 0.8)',
|
||||
borderColor: 'rgba(63, 167, 221, 0.2)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
radar: {
|
||||
center: ['50%', '50%'],
|
||||
radius: '65%',
|
||||
indicator: [],
|
||||
shape: 'circle',
|
||||
splitNumber: 8,
|
||||
axisName: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
width: 2,
|
||||
type: 'dashed',
|
||||
dashOffset: 0
|
||||
}
|
||||
},
|
||||
splitArea: {
|
||||
show: true,
|
||||
areaStyle: {
|
||||
color: ['rgba(255, 255, 255, 0.02)', 'rgba(255, 255, 255, 0.05)']
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: '15%',
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
rotate: 45
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
series: []
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '监测值',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '变化率',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '监测值',
|
||||
type: 'bar',
|
||||
barWidth: 20,
|
||||
data: [],
|
||||
z: 10
|
||||
},
|
||||
{
|
||||
name: '变化率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#67C23A'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' }
|
||||
])
|
||||
},
|
||||
data: [],
|
||||
z: 11
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 添加动画效果
|
||||
let angle = 0
|
||||
let dashOffset = 0
|
||||
const animate = () => {
|
||||
angle = (angle + 0.5) % 360
|
||||
dashOffset = (dashOffset + 1) % 20
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
startAngle: angle,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
dashOffset: -dashOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
animate()
|
||||
|
||||
// 确保图表正确渲染
|
||||
setTimeout(() => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
}, 200)
|
||||
|
||||
return chart
|
||||
}
|
||||
|
||||
@ -172,111 +231,113 @@ const updateChart = () => {
|
||||
const points = processedData.value
|
||||
if (points.length === 0) return
|
||||
|
||||
// 获取所有指标名称和最大值
|
||||
const indicators = new Set()
|
||||
const maxValues = {}
|
||||
// 处理数据
|
||||
const allIndicators = new Set()
|
||||
const indicatorValues = {}
|
||||
const changeRates = {}
|
||||
|
||||
points.forEach(point => {
|
||||
point.indicators.forEach(indicator => {
|
||||
indicators.add(indicator.name)
|
||||
if (!maxValues[indicator.name] || maxValues[indicator.name] < indicator.value) {
|
||||
maxValues[indicator.name] = indicator.value
|
||||
allIndicators.add(indicator.name)
|
||||
if (!indicatorValues[indicator.name]) {
|
||||
indicatorValues[indicator.name] = []
|
||||
}
|
||||
indicatorValues[indicator.name].push(indicator.value)
|
||||
})
|
||||
})
|
||||
|
||||
// 设置雷达图的指示器
|
||||
const radarIndicators = Array.from(indicators).map(name => ({
|
||||
name: name,
|
||||
max: Math.ceil(maxValues[name] * 1.2) // 最大值增加20%的余量
|
||||
}))
|
||||
// 计算变化率
|
||||
allIndicators.forEach(name => {
|
||||
const values = indicatorValues[name]
|
||||
if (values.length >= 2) {
|
||||
const current = values[values.length - 1]
|
||||
const previous = values[values.length - 2]
|
||||
changeRates[name] = previous ? ((current - previous) / previous * 100) : 0
|
||||
} else {
|
||||
changeRates[name] = 0
|
||||
}
|
||||
})
|
||||
|
||||
// 生成系列数据
|
||||
const series = [{
|
||||
type: 'radar',
|
||||
animation: true,
|
||||
animationDuration: 2000,
|
||||
animationEasing: 'quadraticInOut',
|
||||
data: points.map(point => {
|
||||
const pointType = point.type
|
||||
const colors = {
|
||||
water: ['rgba(64, 158, 255, 1)', 'rgba(54, 206, 158, 0.1)'],
|
||||
air: ['rgba(230, 162, 60, 1)', 'rgba(245, 108, 108, 0.1)'],
|
||||
soil: ['rgba(103, 194, 58, 1)', 'rgba(149, 212, 117, 0.1)'],
|
||||
default: ['rgba(64, 158, 255, 1)', 'rgba(54, 206, 158, 0.1)']
|
||||
}
|
||||
const colorSet = colors[pointType] || colors.default
|
||||
// 构建图表数据
|
||||
const xAxisData = Array.from(allIndicators)
|
||||
const barData = xAxisData.map(name => {
|
||||
const values = indicatorValues[name]
|
||||
return values[values.length - 1] || 0
|
||||
})
|
||||
const lineData = xAxisData.map(name => changeRates[name] || 0)
|
||||
|
||||
return {
|
||||
name: point.point,
|
||||
value: radarIndicators.map(indicator => {
|
||||
const ind = point.indicators.find(i => i.name === indicator.name)
|
||||
return ind ? ind.value : 0
|
||||
}),
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: {
|
||||
color: colorSet[0],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
shadowColor: colorSet[0],
|
||||
shadowBlur: 10
|
||||
},
|
||||
// 更新图表配置
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: xAxisData
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '监测值',
|
||||
type: 'bar',
|
||||
barWidth: 20,
|
||||
data: barData.map(value => ({
|
||||
value: 0,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(63, 167, 221, 0.9)' },
|
||||
{ offset: 1, color: 'rgba(63, 167, 221, 0.2)' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}))
|
||||
},
|
||||
{
|
||||
name: '变化率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
color: colorSet[0],
|
||||
width: 2,
|
||||
type: [5, 10],
|
||||
shadowColor: colorSet[0],
|
||||
shadowBlur: 5
|
||||
width: 3,
|
||||
color: '#67C23A'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: colorSet[0] },
|
||||
{ offset: 1, color: colorSet[1] }
|
||||
]),
|
||||
opacity: 0.5
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
scale: true,
|
||||
itemStyle: {
|
||||
shadowBlur: 20
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
data: lineData.map(() => 0)
|
||||
}
|
||||
})
|
||||
}]
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: radarIndicators
|
||||
},
|
||||
series: series
|
||||
]
|
||||
})
|
||||
|
||||
// 添加呼吸动画
|
||||
let breatheEffect = 0
|
||||
const animate = () => {
|
||||
breatheEffect = (breatheEffect + 1) % 100
|
||||
const opacity = 0.3 + Math.sin(breatheEffect * Math.PI / 50) * 0.2
|
||||
|
||||
series[0].data.forEach((item, index) => {
|
||||
chart.setOption({
|
||||
series: [{
|
||||
data: series[0].data.map((dataItem, i) => ({
|
||||
...dataItem,
|
||||
areaStyle: {
|
||||
...dataItem.areaStyle,
|
||||
opacity: i === index ? opacity : 0.3
|
||||
// 延迟显示真实数据
|
||||
setTimeout(() => {
|
||||
chart.setOption({
|
||||
series: [
|
||||
{
|
||||
name: '监测值',
|
||||
data: barData.map(value => ({
|
||||
value,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(63, 167, 221, 0.9)' },
|
||||
{ offset: 1, color: 'rgba(63, 167, 221, 0.2)' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}))
|
||||
}]
|
||||
})
|
||||
})),
|
||||
animationDelay: (idx) => idx * 100,
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicInOut'
|
||||
},
|
||||
{
|
||||
name: '变化率',
|
||||
data: lineData,
|
||||
animationDelay: (idx) => idx * 100 + 500,
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicInOut'
|
||||
}
|
||||
]
|
||||
})
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
animate()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 定时更新数据
|
||||
@ -417,6 +478,19 @@ onUnmounted(() => {
|
||||
.chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.loading::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 24, 65, 0.3);
|
||||
z-index: 10;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -427,4 +501,16 @@ onUnmounted(() => {
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -28,9 +28,9 @@ const initChart = async () => {
|
||||
formatter: function(params) {
|
||||
const item = params[0]
|
||||
return `${item.name}<br/>
|
||||
预警次数:${item.value}次<br/>
|
||||
预警天数:${item.data.days}天<br/>
|
||||
日均预警:${item.data.avg.toFixed(2)}次/天`
|
||||
预警次数:${item.value}次<br/>
|
||||
预警天数:${item.data.days}天<br/>
|
||||
日均预警:${item.data.avg.toFixed(2)}次/天`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
|
@ -119,36 +119,45 @@ onUnmounted(() => {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-family: 'DIN Alternate', 'Arial', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #3fa7dd;
|
||||
text-shadow: 0 0 10px rgba(63, 167, 221, 0.5);
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 24, 65, 0.3);
|
||||
border: 1px solid rgba(63, 167, 221, 0.2);
|
||||
letter-spacing: 1px;
|
||||
backdrop-filter: blur(4px);
|
||||
text-shadow: 0 0 15px rgba(63, 167, 221, 0.6);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 24, 65, 0.5);
|
||||
border: 1px solid rgba(63, 167, 221, 0.3);
|
||||
letter-spacing: 2px;
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
box-shadow: 0 0 20px rgba(63, 167, 221, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 24, 65, 0.7);
|
||||
border-color: rgba(63, 167, 221, 0.5);
|
||||
box-shadow: 0 0 30px rgba(63, 167, 221, 0.3);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #3fa7dd;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s infinite;
|
||||
box-shadow: 0 0 15px rgba(63, 167, 221, 0.8);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0.4);
|
||||
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0.6);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(63, 167, 221, 0);
|
||||
box-shadow: 0 0 0 10px rgba(63, 167, 221, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0);
|
||||
|
@ -241,7 +241,7 @@ const handleUploadSuccess = (response) => {
|
||||
ElMessage.error('上传成功但未获取到图片地址')
|
||||
return
|
||||
}
|
||||
form.value.image_url = imageUrl.startsWith('http') ? imageUrl : `http://localhost:3000${imageUrl}`
|
||||
form.value.image_url = imageUrl.startsWith('http') ? imageUrl : `http://127.0.0.1:3000${imageUrl}`
|
||||
ElMessage.success('图片上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '图片上传失败')
|
||||
@ -408,7 +408,7 @@ onMounted(() => {
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="carousel-uploader"
|
||||
action="http://localhost:3000/api/admin/carousels/upload"
|
||||
action="http://127.0.0.1:3000/api/admin/carousels/upload"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
|
Loading…
x
Reference in New Issue
Block a user