551 lines
15 KiB
Vue
551 lines
15 KiB
Vue
<template>
|
||
<div>
|
||
<!-- 顶部统计卡片区 -->
|
||
<el-row :gutter="16" class="mb-16px">
|
||
<el-col :span="6">
|
||
<el-card shadow="hover" class="stat-card stat-card-total">
|
||
<div class="stat-value">{{ deviceStats.totalCount }}<span class="stat-unit">台</span></div>
|
||
<div class="stat-title">总设备数</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card shadow="hover" class="stat-card stat-card-online">
|
||
<div class="stat-value online">{{ deviceStats.onlineCount }}<span class="stat-unit">台</span></div>
|
||
<div class="stat-title">在线设备</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card shadow="hover" class="stat-card stat-card-offline">
|
||
<div class="stat-value offline">{{ deviceStats.offlineCount }}<span class="stat-unit">台</span></div>
|
||
<div class="stat-title">离线设备</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card shadow="hover" class="stat-card stat-card-warning">
|
||
<div class="stat-value warning">{{ alertData.alertTotal }}<span class="stat-unit">条</span></div>
|
||
<div class="stat-title">预警信息 <span class="stat-warning-unhandled">未处理 {{ alertData.alertUnhandled }}</span></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 下方地图+柱状图 -->
|
||
<el-row :gutter="16">
|
||
<el-col :span="15">
|
||
<el-card shadow="never" class="modern-card" style="height: 650px">
|
||
<template #header>
|
||
<span class="modern-card-title">设备分布地图</span>
|
||
</template>
|
||
<Echart :options="mapOptions" :height="580" />
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="9">
|
||
<el-card shadow="never" class="modern-card" style="height: 650px;">
|
||
<template #header>
|
||
<span class="modern-card-title">会员增长趋势</span>
|
||
</template>
|
||
<Echart :options="memberLineOptions" :height="580" />
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { Echart } from '@/components/Echart'
|
||
import chinaJson from '@/assets/map/json/china.json'
|
||
import * as echarts from 'echarts/core'
|
||
import { ElMessage } from 'element-plus'
|
||
import { DataAnalysis, Connection, CircleClose, Warning } from '@element-plus/icons-vue'
|
||
import { DeviceApi, DeviceMapVO } from '@/api/device'
|
||
import { AlertMessageApi } from '@/api/alertmessage'
|
||
import { PersonApi, MemberGrowthVO } from '@/api/person'
|
||
import { getUserProfile } from '@/api/system/user/profile'
|
||
|
||
// 预警统计数据
|
||
const alertData = reactive({
|
||
alertTotal: 0,
|
||
alertUnhandled: 0
|
||
})
|
||
|
||
// 设备统计数据
|
||
const deviceStats = reactive({
|
||
totalCount: 0,
|
||
onlineCount: 0,
|
||
offlineCount: 0
|
||
})
|
||
|
||
// 地图数据
|
||
const mapData = reactive<DeviceMapVO>({
|
||
totalCount: 0,
|
||
onlineCount: 0,
|
||
offlineCount: 0,
|
||
alertTotal: 0,
|
||
alertUnhandled: 0,
|
||
mapData: [],
|
||
scatterData: []
|
||
})
|
||
|
||
// 地图配置(底图+点标注)
|
||
const mapOptions = ref<any>({})
|
||
|
||
// 会员增长数据
|
||
const memberGrowthData = ref([
|
||
{ date: '周一', count: 0 },
|
||
{ date: '周二', count: 0 },
|
||
{ date: '周三', count: 0 },
|
||
{ date: '周四', count: 0 },
|
||
{ date: '周五', count: 0 },
|
||
{ date: '周六', count: 0 },
|
||
{ date: '周日', count: 0 }
|
||
])
|
||
|
||
const memberLineOptions = ref<any>({
|
||
tooltip: { trigger: 'axis' },
|
||
grid: { left: 40, right: 24, top: 40, bottom: 40 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: memberGrowthData.value.map(d => d.date),
|
||
boundaryGap: false,
|
||
axisLine: { lineStyle: { color: '#dbeafe' } },
|
||
axisLabel: { color: '#666', fontSize: 14 }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
minInterval: 1,
|
||
axisLine: { show: false },
|
||
splitLine: { lineStyle: { color: '#f0f0f0' } },
|
||
axisLabel: { color: '#888', fontSize: 13 }
|
||
},
|
||
series: [
|
||
{
|
||
name: '会员增长',
|
||
type: 'line',
|
||
data: memberGrowthData.value.map(d => d.count),
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 10,
|
||
lineStyle: { color: '#409EFF', width: 3 },
|
||
itemStyle: { color: '#409EFF', borderColor: '#fff', borderWidth: 2 },
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0, y: 0, x2: 0, y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: 'rgba(64,158,255,0.18)' },
|
||
{ offset: 1, color: 'rgba(64,158,255,0.02)' }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
})
|
||
|
||
// 获取预警统计数据
|
||
const getAlertData = async (orgid: number) => {
|
||
try {
|
||
const data = await AlertMessageApi.getAlertStatistics(orgid)
|
||
Object.assign(alertData, data)
|
||
} catch (error) {
|
||
console.error('获取预警数据失败:', error)
|
||
ElMessage.error('获取预警数据失败')
|
||
}
|
||
}
|
||
|
||
// 获取设备统计数据
|
||
const getDeviceStats = async (orgid: number) => {
|
||
try {
|
||
const data = await DeviceApi.getDeviceStatistics(orgid)
|
||
Object.assign(deviceStats, data)
|
||
} catch (error) {
|
||
console.error('获取设备统计数据失败:', error)
|
||
ElMessage.error('获取设备统计数据失败')
|
||
}
|
||
}
|
||
|
||
// 获取设备分布地图数据
|
||
const getDeviceMapData = async (orgid: number) => {
|
||
try {
|
||
const data = await DeviceApi.getDeviceMapData(orgid)
|
||
|
||
|
||
// 更新地图数据
|
||
Object.assign(mapData, data)
|
||
|
||
// 更新地图配置 - 组件会自动监听options变化
|
||
updateMapOptions()
|
||
|
||
|
||
} catch (error) {
|
||
console.error('获取设备分布数据失败:', error)
|
||
ElMessage.error('获取设备分布数据失败')
|
||
}
|
||
}
|
||
|
||
// 获取会员增长数据
|
||
const getMemberGrowthData = async (orgid: number) => {
|
||
try {
|
||
const data = await PersonApi.getMemberGrowthData(orgid)
|
||
memberGrowthData.value = data
|
||
|
||
// 更新会员增长图表
|
||
updateMemberLineOptions()
|
||
} catch (error) {
|
||
console.error('获取会员增长数据失败:', error)
|
||
ElMessage.error('获取会员增长数据失败')
|
||
}
|
||
}
|
||
|
||
// 更新会员增长图表
|
||
const updateMemberLineOptions = () => {
|
||
memberLineOptions.value = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: (params: any) => {
|
||
const data = params[0]
|
||
return `${data.name}<br/>新增会员:${data.value} 人`
|
||
}
|
||
},
|
||
grid: { left: 40, right: 24, top: 40, bottom: 40 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: memberGrowthData.value.map(d => d.date),
|
||
boundaryGap: false,
|
||
axisLine: { lineStyle: { color: '#dbeafe' } },
|
||
axisLabel: { color: '#666', fontSize: 14 }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
minInterval: 1,
|
||
axisLine: { show: false },
|
||
splitLine: { lineStyle: { color: '#f0f0f0' } },
|
||
axisLabel: { color: '#888', fontSize: 13 }
|
||
},
|
||
series: [
|
||
{
|
||
name: '会员增长',
|
||
type: 'line',
|
||
data: memberGrowthData.value.map(d => d.count),
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 10,
|
||
lineStyle: { color: '#409EFF', width: 3 },
|
||
itemStyle: { color: '#409EFF', borderColor: '#fff', borderWidth: 2 },
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0, y: 0, x2: 0, y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: 'rgba(64,158,255,0.18)' },
|
||
{ offset: 1, color: 'rgba(64,158,255,0.02)' }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
// 更新地图配置
|
||
const updateMapOptions = () => {
|
||
console.log('更新地图配置,当前数据:', mapData)
|
||
|
||
const maxValue = mapData.mapData.length > 0
|
||
? Math.max(...mapData.mapData.map(d => d.value), 10)
|
||
: 10
|
||
|
||
console.log('地图数据:', mapData.mapData)
|
||
console.log('散点数据:', mapData.scatterData)
|
||
console.log('最大值:', maxValue)
|
||
|
||
// 处理散点数据,确保格式正确
|
||
const processedScatterData = mapData.scatterData.map(item => {
|
||
console.log('处理散点数据项:', item)
|
||
return {
|
||
name: item.name,
|
||
value: item.value,
|
||
count: item.count
|
||
}
|
||
})
|
||
|
||
console.log('处理后的散点数据:', processedScatterData)
|
||
|
||
mapOptions.value = {
|
||
geo: {
|
||
map: 'china',
|
||
roam: true,
|
||
label: {
|
||
show: true, // 开启geo的label显示
|
||
color: '#2c3e50',
|
||
fontWeight: 'bold',
|
||
fontSize: 11
|
||
},
|
||
itemStyle: {
|
||
areaColor: '#f8f9fa', // 更浅的背景色
|
||
borderColor: '#4ea3ff'
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: (params: any) => {
|
||
if (params.seriesType === 'effectScatter') {
|
||
// 设备点的tooltip
|
||
return `${params.name}<br/>设备数量: ${params.value[2]}台`
|
||
}
|
||
// 注释掉省份区域的tooltip - 需要时可以取消注释
|
||
// if (params.seriesType === 'map') {
|
||
// // 省份区域的tooltip
|
||
// return `${params.name}<br/>设备数量: ${params.data ? params.data.value : 0}台`
|
||
// }
|
||
return params.name
|
||
}
|
||
},
|
||
visualMap: {
|
||
min: 0,
|
||
max: maxValue,
|
||
left: 'left',
|
||
top: 'bottom',
|
||
text: ['多', '少'],
|
||
inRange: { color: ['#e3f2fd', '#1976d2'] }, // 改为更明显的蓝色渐变
|
||
show: true
|
||
},
|
||
series: [
|
||
{
|
||
name: '设备分布',
|
||
type: 'map',
|
||
map: 'china',
|
||
geoIndex: 0,
|
||
roam: true,
|
||
data: mapData.mapData,
|
||
label: {
|
||
show: false // 关闭series的label,使用geo的label
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: 13,
|
||
fontWeight: 'bold',
|
||
color: '#000000' // 强调时使用黑色
|
||
},
|
||
itemStyle: {
|
||
areaColor: '#ffeb3b' // 强调时使用黄色背景
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '设备点',
|
||
type: 'effectScatter',
|
||
coordinateSystem: 'geo',
|
||
data: processedScatterData,
|
||
symbolSize: (val: any) => {
|
||
const size = val[2] || 1
|
||
return Math.max(6, Math.min(16, 6 + size * 2)) // 调整点的大小范围
|
||
},
|
||
showEffectOn: 'render',
|
||
rippleEffect: {
|
||
brushType: 'stroke',
|
||
scale: 2.5, // 减小涟漪效果
|
||
period: 4
|
||
},
|
||
itemStyle: {
|
||
color: '#000000', // 改为黑色
|
||
shadowBlur: 8, // 减小阴影
|
||
shadowColor: '#000000'
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
color: '#333333', // 强调时的深灰色
|
||
shadowBlur: 15,
|
||
shadowColor: '#333333'
|
||
}
|
||
},
|
||
zlevel: 2
|
||
}
|
||
]
|
||
}
|
||
|
||
console.log('地图配置已更新:', mapOptions.value)
|
||
}
|
||
|
||
// 地图点击事件(弹窗显示设备数)
|
||
const handleMapClick = (params: any) => {
|
||
if (params.componentType === 'series') {
|
||
if (params.seriesType === 'map') {
|
||
// 点击省份区域
|
||
const area = params.name
|
||
const count = params.data ? params.data.value : 0
|
||
ElMessage.info(`${area} 有 ${count} 个设备`)
|
||
} else if (params.seriesType === 'effectScatter') {
|
||
// 点击设备点
|
||
const area = params.name
|
||
const count = params.value[2] || 0
|
||
ElMessage.info(`${area} 有 ${count} 个设备`)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 初始化所有数据
|
||
const initAllData = async () => {
|
||
try {
|
||
// 获取用户信息,从中获取orgid(部门ID)
|
||
const userProfile = await getUserProfile()
|
||
const orgid = userProfile.dept.orgid
|
||
|
||
// 并行获取所有数据
|
||
await Promise.all([
|
||
getAlertData(orgid),
|
||
getDeviceStats(orgid),
|
||
getDeviceMapData(orgid),
|
||
getMemberGrowthData(orgid)
|
||
])
|
||
} catch (error) {
|
||
console.error('初始化数据失败:', error)
|
||
ElMessage.error('初始化数据失败')
|
||
}
|
||
}
|
||
|
||
// 注册中国地图并初始化
|
||
onMounted(async () => {
|
||
// 注册中国地图
|
||
// @ts-ignore
|
||
echarts.registerMap('china', chinaJson)
|
||
|
||
// 初始化所有数据
|
||
await initAllData()
|
||
|
||
// 绑定地图点击事件
|
||
setTimeout(() => {
|
||
// 查找所有ECharts实例,第一个应该是地图(因为它在模板中的位置)
|
||
const allCharts = document.querySelectorAll('.v-echart')
|
||
console.log('找到的ECharts元素数量:', allCharts.length)
|
||
|
||
if (allCharts.length > 0) {
|
||
// 第一个ECharts实例应该是设备分布地图
|
||
const mapChart = allCharts[0]
|
||
console.log('地图DOM元素:', mapChart)
|
||
|
||
// @ts-ignore
|
||
const chart = echarts.getInstanceByDom(mapChart)
|
||
console.log('ECharts实例:', chart)
|
||
if (chart) {
|
||
chart.on('click', handleMapClick)
|
||
console.log('地图点击事件已绑定')
|
||
}
|
||
} else {
|
||
console.log('未找到任何ECharts实例')
|
||
}
|
||
}, 1000) // 增加延迟时间,确保地图已渲染
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.stat-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 120px;
|
||
border-radius: 18px;
|
||
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.08);
|
||
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ef 100%);
|
||
transition: box-shadow 0.2s, transform 0.2s;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.stat-card:hover {
|
||
box-shadow: 0 8px 32px 0 rgba(64,158,255,0.18);
|
||
transform: translateY(-2px) scale(1.03);
|
||
}
|
||
.stat-card-total {
|
||
background: linear-gradient(135deg, #e0e7ef 0%, #b6c6e6 100%);
|
||
}
|
||
.stat-card-online {
|
||
background: linear-gradient(135deg, #e8f5e9 0%, #b2f0c0 100%);
|
||
}
|
||
.stat-card-offline {
|
||
background: linear-gradient(135deg, #ffeaea 0%, #ffc1c1 100%);
|
||
}
|
||
.stat-card-warning {
|
||
background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
|
||
}
|
||
.stat-card-warning .stat-icon {
|
||
color: #faad14;
|
||
}
|
||
.stat-value.warning {
|
||
color: #faad14;
|
||
}
|
||
.stat-warning-unhandled {
|
||
color: #f56c6c;
|
||
font-size: 13px;
|
||
margin-left: 8px;
|
||
}
|
||
.stat-icon {
|
||
font-size: 38px;
|
||
margin-bottom: 8px;
|
||
color: #409EFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.stat-card-online .stat-icon {
|
||
color: #4caf50;
|
||
}
|
||
.stat-card-offline .stat-icon {
|
||
color: #f44336;
|
||
}
|
||
.stat-value {
|
||
font-size: 38px;
|
||
font-weight: 700;
|
||
color: #222;
|
||
margin-bottom: 4px;
|
||
line-height: 1.1;
|
||
}
|
||
.stat-value.online {
|
||
color: #4caf50;
|
||
}
|
||
.stat-value.offline {
|
||
color: #f44336;
|
||
}
|
||
.stat-unit {
|
||
font-size: 16px;
|
||
font-weight: 400;
|
||
margin-left: 2px;
|
||
color: #888;
|
||
}
|
||
.stat-title {
|
||
font-size: 16px;
|
||
color: #666;
|
||
letter-spacing: 1px;
|
||
margin-top: 2px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 现代化大卡片样式 */
|
||
.modern-card {
|
||
border-radius: 18px;
|
||
box-shadow: 0 6px 32px 0 rgba(64,158,255,0.10);
|
||
background: #fff;
|
||
border: none;
|
||
overflow: hidden;
|
||
transition: box-shadow 0.2s, transform 0.2s;
|
||
}
|
||
.modern-card:hover {
|
||
box-shadow: 0 12px 48px 0 rgba(64,158,255,0.18);
|
||
transform: translateY(-2px) scale(1.01);
|
||
}
|
||
.modern-card .el-card__header {
|
||
background: transparent;
|
||
border-bottom: none;
|
||
padding: 24px 24px 12px 24px;
|
||
}
|
||
.modern-card-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #222;
|
||
letter-spacing: 1px;
|
||
}
|
||
.modern-card .el-card__body {
|
||
padding: 0 24px 24px 24px;
|
||
}
|
||
</style>
|