Files
shixi2026vue/src/views/signal-coverage/Req7SignalStrengthDistributionView.vue

302 lines
7.7 KiB
Vue
Raw Normal View History

2026-01-13 09:32:02 +08:00
<script setup lang="ts">
import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
// 信号强度数据项接口
interface SignalStrengthItem {
id?: number
userLon?: string
userLat?: string
rssi?: number // 确保 rssi 是 number
[key: string]: any // 允许其他字段
}
const mapContainer = ref<HTMLDivElement | null>(null)
const networkType = ref<string>('ALL')
const networkName = ref<string>('ALL')
const startDate = ref<string>('')
const endDate = ref<string>('')
const loading = ref<boolean>(false)
const dataLoaded = ref<boolean>(false)
let map: any = null
let heatmapOverlay: any = null
// 将后端返回的数据转换为热力图库需要的格式
const transformDataForHeatmap = (data: SignalStrengthItem[]) => {
return data
.filter(item => {
const lat = parseFloat(String(item.userLat))
const lon = parseFloat(String(item.userLon))
// 过滤掉无效坐标和无效信号强度
return !isNaN(lat) && !isNaN(lon) && lat !== 0 && lon !== 0 && item.rssi != null
})
.map(item => {
// 热力图权重需要是正数我们将RSSI-120到0映射到一个正数范围例如1到100
// 信号越强越接近0权重越高
const rssi = item.rssi ?? -120;
const count = Math.max(1, 100 - (Math.abs(rssi) - 20));
return {
lng: parseFloat(String(item.userLon)),
lat: parseFloat(String(item.userLat)),
count: count
}
})
}
// 渲染热力图
const renderHeatmap = (points: any[]) => {
if (!map) return
// 如果已存在热力图层,先移除
if (heatmapOverlay) {
map.removeOverlay(heatmapOverlay)
}
if (points.length === 0) {
return; // 没有数据点,不创建图层
}
// 创建热力图实例
heatmapOverlay = new window.BMapLib.HeatmapOverlay({ radius: 20, opacity: 0.8 })
map.addOverlay(heatmapOverlay)
// 设置热力图数据
heatmapOverlay.setDataSet({
data: points,
max: 100 // 设置权重的最大值
});
// 自动调整地图视野以包含所有点
const viewport = map.getViewport(points.map((p: any) => new window.BMap.Point(p.lng, p.lat)));
map.centerAndZoom(viewport.center, viewport.zoom);
}
// 获取数据
const fetchData = async () => {
loading.value = true
dataLoaded.value = false
try {
const params = new URLSearchParams()
params.append('networkType', networkType.value)
params.append('networkName', networkName.value)
if (startDate.value) params.append('startTime', new Date(startDate.value).getTime().toString())
if (endDate.value) {
const end = new Date(endDate.value);
end.setHours(23, 59, 59, 999);
params.append('endTime', end.getTime().toString());
}
const response = await fetch(`http://localhost:8081/api/signal/heatmap?${params.toString()}`)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const responseData = await response.json()
if (responseData.code !== 200) throw new Error(responseData.message || '获取数据失败')
const heatmapPoints = transformDataForHeatmap(responseData.data || [])
if (map) {
renderHeatmap(heatmapPoints)
}
} catch (error) {
console.error('获取数据失败:', error)
if(map) renderHeatmap([]); // 清空热力图
} finally {
loading.value = false
dataLoaded.value = true
}
}
// 初始化地图
const initMap = async () => {
await nextTick()
// 轮询检查百度地图API和热力图库是否都已加载
if (!window.BMap || !window.BMapLib?.HeatmapOverlay) {
console.warn('百度地图API或热力图库未加载200ms后重试...')
setTimeout(initMap, 200)
return
}
if (!mapContainer.value) {
console.warn('地图容器未找到')
return
}
map = new window.BMap.Map(mapContainer.value)
map.centerAndZoom(new window.BMap.Point(105.8951, 36.5667), 5) // 默认中国中心
map.enableScrollWheelZoom(true)
map.addControl(new window.BMap.MapTypeControl({ mapTypes: [window.BMap.MapTypeId.NORMAL, window.BMap.MapTypeId.SATELLITE] }))
map.addControl(new window.BMap.NavigationControl())
// 地图初始化后立即获取数据
fetchData()
}
// 监听筛选条件变化
watch([networkType, networkName, startDate, endDate], () => {
if (map && dataLoaded.value) {
fetchData()
}
})
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (heatmapOverlay && map) {
map.removeOverlay(heatmapOverlay)
}
map = null
})
</script>
<template>
<div class="page">
2026-01-13 09:32:02 +08:00
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<p class="loading-text">加载数据中...</p>
</div>
</div>
<h2>业务需求7信号覆盖-信号强度分布热力图</h2>
<div class="filter-bar">
<div class="filter-item">
<label for="networkType">运营商:</label>
<select id="networkType" v-model="networkType" class="select-input" :disabled="loading">
<option value="ALL">ALL</option>
<option value="CMCC">中国移动</option>
<option value="CUCC">中国联通</option>
<option value="CTCC">中国电信</option>
</select>
</div>
<div class="filter-item">
<label for="networkName">网络类型:</label>
<select id="networkName" v-model="networkName" class="select-input" :disabled="loading">
<option value="ALL">ALL</option>
<option value="2G">2G</option>
<option value="3G">3G</option>
<option value="4G">4G</option>
<option value="5G">5G</option>
</select>
</div>
<div class="filter-item">
<label for="startDate">起始日期:</label>
<input id="startDate" type="date" v-model="startDate" class="date-input" :disabled="loading" />
</div>
<div class="filter-item">
<label for="endDate">结束日期:</label>
<input id="endDate" type="date" v-model="endDate" class="date-input" :disabled="loading" />
</div>
<button @click="fetchData" class="refresh-btn" :disabled="loading">
{{ loading ? '加载中...' : '刷新' }}
</button>
</div>
<div class="map-container" ref="mapContainer"></div>
</div>
</template>
<style scoped>
.page {
padding: 16px;
2026-01-13 09:32:02 +08:00
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
2026-01-13 09:32:02 +08:00
h2 {
margin: 0 0 16px 0;
font-size: 20px;
font-weight: 600;
}
.filter-bar {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 6px;
flex-wrap: wrap;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.select-input,
.date-input {
padding: 6px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
}
.refresh-btn {
padding: 6px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.refresh-btn:hover:not(:disabled) {
background: #2563eb;
}
.refresh-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.map-container {
width: 100%;
height: calc(100vh - 220px);
min-height: 500px;
border: 1px solid #e5e7eb;
border-radius: 4px;
flex: 1;
}
.loading-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 16px;
color: #374151;
margin: 0;
}
</style>