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