vue3/src/views/Home/Index.vue

551 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>