Files
shixi2026vue/src/views/data-connection/Req15RateDistributionView.vue

399 lines
10 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 DataConnectionItem {
id: number
imei: string | null
networkType: string | null
wifiBssid: string | null
wifiState: string | null
wifiRssi: string | null
mobileState: string | null
mobileNetworkType: string | null
networkId: string | null
gsmStrength: string | null
cdmaDbm: string | null
evdoDbm: string | null
internalIp: string | null
webUrl: string | null
pingValue: string
userLat: string
userLon: string
userLocationInfo: string | null
bsLat: string | null
bsLon: string | null
timeIndexClient: string | null
version: string | null
timeServerInsert: string | null
ds: string | null
dt: string | null
}
const mapContainer = ref<HTMLDivElement | null>(null)
const operator = ref<string>('CMCC') // 默认选择中国移动
const date = ref<string>('')
const loading = ref<boolean>(false)
const dataLoaded = ref<boolean>(false)
const dataList = ref<DataConnectionItem[]>([])
const markers: any[] = []
let map: any = null
// 存储所有有效坐标点(带原数据)
let validPointsList: Array<{ lat: number; lon: number; item: DataConnectionItem }> = []
// 核心函数:默认聚焦到第一个有效点并放大
const calculateDensityFocus = (): { center: any; zoom: number; hasValid: boolean } => {
if (!window.BMap || validPointsList.length === 0) {
return {
center: new window.BMap.Point(105.8951, 36.5667), // 中国中心
zoom: 4,
hasValid: false
}
}
// 默认聚焦到第一个有效点并放大到17级更大的缩放级别
const firstPoint = validPointsList[0]
return {
center: new window.BMap.Point(firstPoint.lon, firstPoint.lat),
zoom: 17, // 放大到17级可以看到更详细的区域
hasValid: true
}
}
// 清除标记点
const clearMarkers = () => {
if (!map) return
// 遍历所有标记点并移除
markers.forEach(marker => {
try {
map.removeOverlay(marker)
} catch (error) {
console.warn('移除标记点失败:', error)
}
})
// 清空标记点数组
markers.length = 0
}
// 获取标记点颜色
const getMarkerColor = (pingValue: string): string => {
if (pingValue === '11111') return '#10b981'
if (pingValue === '00000') return '#6b7280'
return '#3b82f6'
}
// 添加标记点并自动聚焦
const addMarkersAndFocus = () => {
if (!map || validPointsList.length === 0) return
clearMarkers()
// 批量添加标记点
validPointsList.forEach(({ lat, lon, item }) => {
const point = new window.BMap.Point(lon, lat)
const color = getMarkerColor(item.pingValue)
// 自定义图标(固定大小,确保缩放后可见)
const icon = new window.BMap.Icon(
`data:image/svg+xml;base64,${btoa(`
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="8" fill="${color}" stroke="white" stroke-width="2"/>
</svg>
`)}`,
new window.BMap.Size(20, 20),
{ anchor: new window.BMap.Size(10, 10) }
)
const marker = new window.BMap.Marker(point, { icon })
// 信息窗口
const infoWindow = new window.BMap.InfoWindow(`
<div style="padding:8px">
<p><strong>网络ID:</strong> ${item.networkId || 'N/A'}</p>
<p><strong>类型:</strong> ${item.networkType || 'N/A'}</p>
<p><strong>Ping值:</strong> ${item.pingValue}</p>
<p><strong>坐标:</strong> ${lat.toFixed(6)}, ${lon.toFixed(6)}</p>
</div>
`, { width: 200 })
marker.addEventListener('click', () => map.openInfoWindow(infoWindow, point))
map.addOverlay(marker)
markers.push(marker)
})
// 标记点添加完成后,立即聚焦密度中心(关键:同步执行,无延迟)
const { center, zoom } = calculateDensityFocus()
map.centerAndZoom(center, zoom)
}
// 过滤有效坐标点(中国境内)
const filterValidPoints = (data: DataConnectionItem[]) => {
validPointsList = data.filter(item => {
if (!item.userLat || !item.userLon) return false
const lat = parseFloat(item.userLat)
const lon = parseFloat(item.userLon)
// 中国经纬度范围纬度18°N-53°N经度73°E-135°E
return !isNaN(lat) && !isNaN(lon) && lat >= 18 && lat <= 53 && lon >= 73 && lon <= 135
}).map(item => ({
lat: parseFloat(item.userLat!),
lon: parseFloat(item.userLon!),
item
}))
}
// 获取数据
const fetchData = async () => {
loading.value = true
dataLoaded.value = false
// 先清除旧数据:清空有效点列表和地图上的标记点
validPointsList = []
if (map) {
clearMarkers()
}
try {
const body: { operator?: string; date?: string } = {}
if (operator.value) body.operator = operator.value
if (date.value) body.date = date.value
const response = await fetch('http://localhost:8081/dataConnection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const data = await response.json() as DataConnectionItem[]
dataList.value = data
// 过滤有效点
filterValidPoints(data)
// 地图已初始化:直接添加标记点并聚焦
if (map) {
await nextTick()
addMarkersAndFocus()
}
} catch (error) {
console.error('获取数据失败:', error)
validPointsList = []
clearMarkers()
} finally {
loading.value = false
dataLoaded.value = true
}
}
// 初始化地图(必须在数据加载完成后执行!)
const initMapAfterData = async () => {
// 等待DOM渲染和百度地图API加载
await nextTick()
// 检查百度地图API是否加载完成
if (!window.BMap || !window.BMap.Map) {
console.warn('百度地图API未加载等待中...')
setTimeout(() => initMapAfterData(), 200)
return
}
if (!mapContainer.value) {
console.warn('地图容器未找到')
return
}
// 如果地图已存在,先清除
if (map) {
clearMarkers()
}
// 初始化地图
map = new window.BMap.Map(mapContainer.value)
map.enableScrollWheelZoom(true)
map.enableDragging(true)
// 添加地图控件
map.addControl(new window.BMap.MapTypeControl({
mapTypes: [window.BMap.MapTypeId.NORMAL, window.BMap.MapTypeId.SATELLITE]
}))
map.addControl(new window.BMap.NavigationControl())
map.addControl(new window.BMap.ScaleControl())
// 如果有数据,添加标记点并聚焦
if (validPointsList.length > 0) {
// 等待地图完全初始化后再添加标记点
setTimeout(() => {
addMarkersAndFocus()
}, 100)
} else {
// 无数据时显示全国地图
map.centerAndZoom(new window.BMap.Point(105.8951, 36.5667), 4)
}
}
// 监听筛选条件变化
watch([operator, date], () => {
if (map && dataLoaded.value) fetchData()
})
onMounted(async () => {
// 第一步:先加载数据
await fetchData()
// 第二步:数据加载完成后,再初始化地图
await initMapAfterData()
})
onUnmounted(() => {
clearMarkers()
map = null
})
</script>
<template>
<div class="page">
<!-- 加载遮罩 -->
<div v-if="loading || !dataLoaded" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<p class="loading-text">加载数据中...</p>
</div>
</div>
<h2>业务需求15数据链接-数据链接率分布</h2>
<div class="filter-bar">
<div class="filter-item">
<label for="operator">运营商:</label>
<select id="operator" v-model="operator" class="select-input" :disabled="loading">
<option value="CMCC">中国移动</option>
<option value="CUCC">中国联通</option>
<option value="CTCC">中国电信</option>
</select>
</div>
<div class="filter-item">
<label for="date">日期:</label>
<input id="date" type="date" v-model="date" class="date-input" :disabled="loading" />
</div>
<button @click="fetchData" class="refresh-btn" :disabled="loading">
{{ loading ? '加载中...' : '刷新' }}
</button>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-dot" style="background-color: #10b981;"></span>
<span>正常 (11111)</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background-color: #6b7280;"></span>
<span>异常 (00000)</span>
</div>
</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; }
.legend {
display: flex;
gap: 24px;
margin-bottom: 12px;
padding: 8px 12px;
background: #f9fafb;
border-radius: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid white;
box-shadow: 0 0 0 1px rgba(0,0,0,0.1);
}
.map-container {
width: 100%;
height: calc(100vh - 280px);
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>