vue3/src/views/Home/Index.vue
2025-06-18 16:11:10 +08:00

404 lines
11 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">{{ 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">{{ 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">{{ 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">{{ alertTotal }}<span class="stat-unit"></span></div>
<div class="stat-title">预警信息 <span class="stat-warning-unhandled">未处理 {{ alertUnhandled }}</span></div>
</el-card>
</el-col>
</el-row>
<!-- 下方地图+柱状图 -->
<el-row :gutter="16">
<el-col :span="16">
<el-card shadow="never" class="modern-card" style="height: 700px">
<template #header>
<span class="modern-card-title">设备分布地图</span>
</template>
<Echart :options="mapOptions" :height="580" />
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="modern-card" style="height: 700px;">
<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 { EChartsOption } from 'echarts'
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'
// 假设设备数据实际可从API获取
const devices = [
{ id: 1, name: '设备A', address: '上海市', status: 'online', type: '传感器' },
{ id: 2, name: '设备B', address: '上海市', status: 'offline', type: '摄像头' },
{ id: 3, name: '设备C', address: '北京市', status: 'online', type: '传感器' },
{ id: 4, name: '设备D', address: '广东省', status: 'offline', type: '摄像头' },
{ id: 5, name: '设备E', address: '广东省', status: 'online', type: '传感器' },
{ id: 6, name: '设备F', address: '江苏省', status: 'online', type: '摄像头' }
]
// 假设预警数据实际可从API获取
const alertList = [
{ id: 1, content: '温度过高', status: 0 },
{ id: 2, content: '电量过低', status: 1 },
{ id: 3, content: '设备离线', status: 0 }
]
const alertTotal = alertList.length
const alertUnhandled = alertList.filter(a => a.status === 0).length
// 统计卡片数据
const totalCount = devices.length
const onlineCount = devices.filter(d => d.status === 'online').length
const offlineCount = devices.filter(d => d.status === 'offline').length
// 设备按省份聚合
const areaCount: Record<string, number> = {}
devices.forEach(device => {
// 只取省份(如"上海市"、"广东省"
const match = device.address.match(/(.*?[省市自治区])/)
const area = match ? match[1] : device.address
areaCount[area] = (areaCount[area] || 0) + 1
})
const mapData = Object.entries(areaCount).map(([name, value]) => ({ name, value }))
// 省会经纬度表(部分示例,可补充完整)
const provinceCoords: Record<string, [number, number]> = {
'北京市': [116.4074, 39.9042],
'上海市': [121.4737, 31.2304],
'广东省': [113.2665, 23.1322],
'江苏省': [118.7969, 32.0603],
// ...可补充更多省份
}
// effectScatter 数据
const scatterData = mapData
.filter(item => provinceCoords[item.name])
.map(item => ({
name: item.name,
value: [...provinceCoords[item.name], item.value],
count: item.value
}))
// 地图配置(底图+点标注)
const mapOptions = ref<EChartsOption>({})
// 饼状图:设备类型分布
const typeCount: Record<string, number> = {}
devices.forEach(device => {
typeCount[device.type] = (typeCount[device.type] || 0) + 1
})
const pieData = Object.entries(typeCount).map(([name, value]) => ({ name, value }))
const barOptions = ref<any>({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: {
orient: 'vertical',
left: 'left',
data: Object.keys(typeCount)
},
series: [
{
name: '设备类型分布',
type: 'pie',
radius: '70%',
center: ['60%', '50%'],
data: pieData,
label: {
formatter: '{b}: {c} ({d}%)',
fontSize: 14
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
// 会员一周增长模拟数据
const memberGrowthData = [
{ date: '周一', count: 12 },
{ date: '周二', count: 18 },
{ date: '周三', count: 25 },
{ date: '周四', count: 20 },
{ date: '周五', count: 30 },
{ date: '周六', count: 40 },
{ date: '周日', count: 35 }
]
const memberLineOptions = ref({
tooltip: { trigger: 'axis' },
grid: { left: 40, right: 24, top: 40, bottom: 40 },
xAxis: {
type: 'category',
data: memberGrowthData.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.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)' }
]
}
}
}
]
})
// 注册中国地图
onMounted(() => {
// @ts-ignore
echarts.registerMap('china', chinaJson)
mapOptions.value = {
geo: {
map: 'china',
roam: true,
label: {
show: true,
color: '#fff',
fontWeight: 'bold',
fontSize: 14,
formatter: (params) => params.name
},
itemStyle: { areaColor: '#232c3b', borderColor: '#4ea3ff' }
},
tooltip: {
trigger: 'item',
formatter: (params: any) => {
if (params.seriesType === 'effectScatter') {
return `${params.name}<br/>设备数量: ${params.value[2]}`
}
if (params.seriesType === 'map') {
return `${params.name}<br/>设备数量: ${params.data ? params.data.value : 0}`
}
return params.name
}
},
visualMap: {
min: 0,
max: Math.max(...mapData.map(d => d.value), 10),
left: 'left',
top: 'bottom',
text: ['多', '少'],
inRange: { color: ['#e0ffff', '#409EFF'] },
show: true
},
series: [
{
name: '设备分布',
type: 'map',
map: 'china',
geoIndex: 0,
roam: true,
data: mapData,
emphasis: {
label: { show: true }
}
},
{
name: '设备点',
type: 'effectScatter',
coordinateSystem: 'geo',
data: scatterData,
symbolSize: (val) => 16 + Math.sqrt(val[2]) * 2,
showEffectOn: 'render',
rippleEffect: { brushType: 'stroke' },
itemStyle: {
color: '#ff5722',
shadowBlur: 10,
shadowColor: '#333'
},
zlevel: 2
}
]
}
})
// 地图点击事件(弹窗显示设备数)
const handleMapClick = (params: any) => {
if (params.componentType === 'series') {
const area = params.name
const count = params.value
ElMessage.info(`${area}${count} 个设备`)
}
}
// 绑定事件
onMounted(() => {
setTimeout(() => {
const chartDom = document.querySelector('.stat-card ~ .el-row .el-card .echart')
if (chartDom) {
// @ts-ignore
const chart = echarts.getInstanceByDom(chartDom)
chart && chart.on('click', handleMapClick)
}
}, 500)
})
</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>