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

399 lines
10 KiB
Vue
Raw Normal View History

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