Files
shixi2026vue/src/views/signal-coverage/Req7SignalStrengthDistributionView.vue
2026-01-13 09:32:02 +08:00

302 lines
7.7 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.
<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">
<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;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
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>