添加百度地图类型声明,更新 HTML 以加载百度地图 API,新增多个视图组件和路由配置以支持信号覆盖、网络质量、数据连接、热门应用和个人用户的统计展示。
This commit is contained in:
15
src/views/AboutView.vue
Normal file
15
src/views/AboutView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>关于</h2>
|
||||
<p>这里是关于页面内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/ConnectionPointView.vue
Normal file
15
src/views/ConnectionPointView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>连接点</h2>
|
||||
<p>这里用于展示连接点相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/DataConnectionView.vue
Normal file
15
src/views/DataConnectionView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>数据连接</h2>
|
||||
<p>这里用于展示数据连接相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/HomeView.vue
Normal file
15
src/views/HomeView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>仪表盘</h2>
|
||||
<p>这里是总览仪表盘页面,可展示整体概览图表。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/HotAppView.vue
Normal file
15
src/views/HotAppView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>热门 App</h2>
|
||||
<p>这里用于展示热门 App 相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/HotPhoneView.vue
Normal file
15
src/views/HotPhoneView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>热门手机</h2>
|
||||
<p>这里用于展示热门手机相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/NetworkQualityView.vue
Normal file
15
src/views/NetworkQualityView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>网络质量</h2>
|
||||
<p>这里用于展示网络质量相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/PersonalUserView.vue
Normal file
15
src/views/PersonalUserView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>个人用户</h2>
|
||||
<p>这里用于展示个人用户相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/SignalCoverageView.vue
Normal file
15
src/views/SignalCoverageView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>信号覆盖</h2>
|
||||
<p>这里用于展示信号覆盖相关的图表和统计。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
375
src/views/connection-point/Req28ConnectionRankingView.vue
Normal file
375
src/views/connection-point/Req28ConnectionRankingView.vue
Normal file
@@ -0,0 +1,375 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, nextTick, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
interface RegionStatItem {
|
||||
region: string
|
||||
operator: string
|
||||
count: number
|
||||
}
|
||||
|
||||
const chartContainer = ref<HTMLDivElement | null>(null)
|
||||
const chartInstance = ref<echarts.ECharts | null>(null)
|
||||
|
||||
// 运营商筛选:CMCC / CUCC / CTCC / ALL
|
||||
const operator = ref<string>('ALL')
|
||||
// 起止日期(默认都为空)
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// 运营商名称映射
|
||||
const operatorMap: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信',
|
||||
ALL: '全部运营商'
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartContainer.value) return
|
||||
|
||||
// 已有实例先销毁
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose()
|
||||
}
|
||||
|
||||
chartInstance.value = echarts.init(chartContainer.value)
|
||||
}
|
||||
|
||||
// 更新柱状图(前十名)
|
||||
const updateChart = (data: RegionStatItem[]) => {
|
||||
if (!chartInstance.value) return
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.value.setOption({
|
||||
title: { text: '暂无数据' },
|
||||
xAxis: { data: [] },
|
||||
series: [{ type: 'bar', data: [] }]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 按 count 从大到小排序,取前十
|
||||
const top10 = [...data]
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10)
|
||||
|
||||
const regions = top10.map(item => item.region)
|
||||
const counts = top10.map(item => item.count)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: `连接点 TOP10 排名(${operatorMap[operator.value] || '全部运营商'})`,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: (params: any) => {
|
||||
const idx = params[0].dataIndex
|
||||
const item = top10[idx]
|
||||
return `
|
||||
<div style="padding:8px;">
|
||||
<div><strong>${item.region}</strong></div>
|
||||
<div>运营商:${operatorMap[item.operator] || item.operator}</div>
|
||||
<div>连接点数量:<strong>${item.count}</strong></div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: regions,
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
rotate: 30 // 防止省份名过长重叠
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '连接点数量',
|
||||
minInterval: 1
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '连接点数量',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#67b7dc' },
|
||||
{ offset: 0.5, color: '#3498db' },
|
||||
{ offset: 1, color: '#2980b9' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#2980b9' },
|
||||
{ offset: 0.7, color: '#2980b9' },
|
||||
{ offset: 1, color: '#67b7dc' }
|
||||
])
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.value.setOption(option)
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const body: { operator?: string; startDate?: string; endDate?: string } = {}
|
||||
// ALL 不传 operator 字段,其它情况传具体值
|
||||
if (operator.value !== 'ALL') {
|
||||
body.operator = operator.value
|
||||
}
|
||||
// 起始日期不为空时,加入时间范围
|
||||
if (startDate.value) {
|
||||
body.startDate = startDate.value
|
||||
}
|
||||
if (endDate.value) {
|
||||
body.endDate = endDate.value
|
||||
}
|
||||
|
||||
const response = await fetch('http://localhost:8081/nwQuality/regionStatistics', {
|
||||
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 RegionStatItem[]
|
||||
|
||||
// 控制台输出返回数据,便于调试
|
||||
console.log('区域统计返回数据:', data)
|
||||
|
||||
await nextTick()
|
||||
updateChart(data)
|
||||
} catch (error) {
|
||||
console.error('获取区域统计数据失败:', error)
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.setOption({
|
||||
title: { text: '数据加载失败' },
|
||||
xAxis: { data: [] },
|
||||
series: [{ type: 'bar', data: [] }]
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口缩放自适应
|
||||
const handleResize = () => {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听运营商筛选变化,自动刷新数据
|
||||
watch(operator, () => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
initChart()
|
||||
await fetchData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose()
|
||||
chartInstance.value = 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>业务需求28:连接点-连接点排名</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="ALL">ALL(全部)</option>
|
||||
<option value="CMCC">CMCC(中国移动)</option>
|
||||
<option value="CUCC">CUCC(中国联通)</option>
|
||||
<option value="CTCC">CTCC(中国电信)</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 class="refresh-btn" @click="fetchData" :disabled="loading">
|
||||
{{ loading ? '加载中...' : '刷新' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" ref="chartContainer"></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;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
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>
|
||||
@@ -0,0 +1,384 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
|
||||
|
||||
interface NwQualityItem {
|
||||
id: number
|
||||
gpsLat: string | null
|
||||
gpsLon: string | null
|
||||
nwOperator: string | null
|
||||
ulSpeed: number | null
|
||||
dlSpeed: number | null
|
||||
latency: number | null
|
||||
province: string | null
|
||||
daytime: number | null
|
||||
nwType: string | null
|
||||
landmark: string | null
|
||||
companyModel: string | null
|
||||
}
|
||||
|
||||
const mapContainer = ref<HTMLDivElement | null>(null)
|
||||
const operator = ref<string>('ALL')
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
const dataList = ref<NwQualityItem[]>([])
|
||||
|
||||
let map: any = null
|
||||
const markers: any[] = []
|
||||
let validPoints: Array<{ lat: number; lon: number; item: NwQualityItem }> = []
|
||||
|
||||
// 运营商名称映射
|
||||
const operatorMap: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信'
|
||||
}
|
||||
|
||||
// 清除地图上的标记
|
||||
const clearMarkers = () => {
|
||||
if (!map) return
|
||||
markers.forEach(marker => {
|
||||
try {
|
||||
map.removeOverlay(marker)
|
||||
} catch (e) {
|
||||
console.warn('移除标记失败:', e)
|
||||
}
|
||||
})
|
||||
markers.length = 0
|
||||
}
|
||||
|
||||
// 过滤出中国境内的有效经纬度点
|
||||
const filterValidPoints = (data: NwQualityItem[]) => {
|
||||
validPoints = data
|
||||
.filter(item => item.gpsLat && item.gpsLon)
|
||||
.map(item => {
|
||||
const lat = parseFloat(item.gpsLat as string)
|
||||
const lon = parseFloat(item.gpsLon as string)
|
||||
return { lat, lon, item }
|
||||
})
|
||||
.filter(
|
||||
p =>
|
||||
!isNaN(p.lat) &&
|
||||
!isNaN(p.lon) &&
|
||||
p.lat >= 18 &&
|
||||
p.lat <= 53 &&
|
||||
p.lon >= 73 &&
|
||||
p.lon <= 135
|
||||
)
|
||||
}
|
||||
|
||||
// 根据运营商获取标记颜色
|
||||
const getMarkerColor = (nwOperator: string | null): string => {
|
||||
if (nwOperator === 'CMCC') return '#10b981'
|
||||
if (nwOperator === 'CUCC') return '#3b82f6'
|
||||
if (nwOperator === 'CTCC') return '#f97316'
|
||||
return '#6b7280'
|
||||
}
|
||||
|
||||
// 添加标记并自动聚焦到某一个点(第一个点)
|
||||
const addMarkersAndFocus = () => {
|
||||
if (!map || validPoints.length === 0) return
|
||||
|
||||
clearMarkers()
|
||||
|
||||
validPoints.forEach(({ lat, lon, item }) => {
|
||||
const point = new (window as any).BMap.Point(lon, lat)
|
||||
const color = getMarkerColor(item.nwOperator)
|
||||
|
||||
// 使用简单的圆形 SVG 作为标记图标
|
||||
const icon = new (window as any).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 as any).BMap.Size(20, 20),
|
||||
{ anchor: new (window as any).BMap.Size(10, 10) }
|
||||
)
|
||||
|
||||
const marker = new (window as any).BMap.Marker(point, { icon })
|
||||
|
||||
const infoHtml = `
|
||||
<div style="padding:8px;">
|
||||
<div><strong>省份:</strong>${item.province || '未知'}</div>
|
||||
<div><strong>运营商:</strong>${operatorMap[item.nwOperator || ''] || item.nwOperator || '未知'}</div>
|
||||
<div><strong>上行速率:</strong>${item.ulSpeed ?? 'N/A'}</div>
|
||||
<div><strong>下行速率:</strong>${item.dlSpeed ?? 'N/A'}</div>
|
||||
<div><strong>时延:</strong>${item.latency ?? 'N/A'}</div>
|
||||
<div><strong>地标:</strong>${item.landmark || '无'}</div>
|
||||
</div>
|
||||
`
|
||||
const infoWindow = new (window as any).BMap.InfoWindow(infoHtml, { width: 260 })
|
||||
|
||||
marker.addEventListener('click', () => {
|
||||
map.openInfoWindow(infoWindow, point)
|
||||
})
|
||||
|
||||
map.addOverlay(marker)
|
||||
markers.push(marker)
|
||||
})
|
||||
|
||||
// 自动聚焦到第一个有效点,并放大
|
||||
const first = validPoints[0]
|
||||
if (first) {
|
||||
const center = new (window as any).BMap.Point(first.lon, first.lat)
|
||||
map.centerAndZoom(center, 15)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化百度地图
|
||||
const initMap = async () => {
|
||||
await nextTick()
|
||||
if (!mapContainer.value) return
|
||||
|
||||
if (!(window as any).BMap) {
|
||||
console.error('百度地图 API 未加载')
|
||||
return
|
||||
}
|
||||
|
||||
map = new (window as any).BMap.Map(mapContainer.value)
|
||||
map.enableScrollWheelZoom(true)
|
||||
map.enableDragging(true)
|
||||
map.addControl(new (window as any).BMap.NavigationControl())
|
||||
map.addControl(new (window as any).BMap.ScaleControl())
|
||||
map.addControl(
|
||||
new (window as any).BMap.MapTypeControl({
|
||||
mapTypes: [(window as any).BMap.MapTypeId.NORMAL, (window as any).BMap.MapTypeId.SATELLITE]
|
||||
})
|
||||
)
|
||||
|
||||
// 默认显示全国视图
|
||||
map.centerAndZoom(new (window as any).BMap.Point(105.8951, 36.5667), 4)
|
||||
|
||||
// 如果此时已经有数据,直接绘制
|
||||
if (validPoints.length > 0) {
|
||||
addMarkersAndFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const body: { operator?: string; startDate?: string; endDate?: string } = {}
|
||||
|
||||
if (operator.value !== 'ALL') {
|
||||
body.operator = operator.value
|
||||
}
|
||||
if (startDate.value) {
|
||||
body.startDate = startDate.value
|
||||
}
|
||||
if (endDate.value) {
|
||||
body.endDate = endDate.value
|
||||
}
|
||||
|
||||
const response = await fetch('http://localhost:8081/nwQuality/all', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
let data = (await response.json()) as NwQualityItem[]
|
||||
|
||||
// 如果返回数据超过 100 条,只保留前 100 条
|
||||
if (data.length > 100) {
|
||||
console.log(`返回 ${data.length} 条记录,截取前 100 条用于渲染`)
|
||||
data = data.slice(0, 100)
|
||||
}
|
||||
|
||||
console.log('网络质量地理分布数据(用于渲染):', data)
|
||||
|
||||
dataList.value = data
|
||||
filterValidPoints(data)
|
||||
|
||||
if (map) {
|
||||
addMarkersAndFocus()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取网络质量地理分布数据失败:', error)
|
||||
dataList.value = []
|
||||
validPoints = []
|
||||
clearMarkers()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initMap()
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearMarkers()
|
||||
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>业务需求29:连接点-连接点地理分布</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="ALL">ALL(全部)</option>
|
||||
<option value="CMCC">CMCC(中国移动)</option>
|
||||
<option value="CUCC">CUCC(中国联通)</option>
|
||||
<option value="CTCC">CTCC(中国电信)</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 class="refresh-btn" @click="fetchData" :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;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
399
src/views/data-connection/Req15RateDistributionView.vue
Normal file
399
src/views/data-connection/Req15RateDistributionView.vue
Normal file
@@ -0,0 +1,399 @@
|
||||
<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>
|
||||
332
src/views/data-connection/Req16RateStatView.vue
Normal file
332
src/views/data-connection/Req16RateStatView.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
interface ConnectionRateItem {
|
||||
operator: string
|
||||
connectionRate: number
|
||||
totalCount: number
|
||||
successCount: number
|
||||
}
|
||||
|
||||
const chartContainer = ref<HTMLDivElement | null>(null)
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
const chartInstance = ref<echarts.ECharts | null>(null)
|
||||
|
||||
// 运营商名称映射
|
||||
const operatorMap: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信'
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartContainer.value) return
|
||||
|
||||
// 如果图表已存在,先销毁
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose()
|
||||
}
|
||||
|
||||
chartInstance.value = echarts.init(chartContainer.value)
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = (data: ConnectionRateItem[]) => {
|
||||
if (!chartInstance.value) return
|
||||
|
||||
// 按运营商代码排序,确保顺序一致
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
const order = ['CMCC', 'CUCC', 'CTCC']
|
||||
return order.indexOf(a.operator) - order.indexOf(b.operator)
|
||||
})
|
||||
|
||||
const operators = sortedData.map(item => operatorMap[item.operator] || item.operator)
|
||||
const rates = sortedData.map(item => item.connectionRate)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '各运营商数据连接率统计',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const dataIndex = params[0].dataIndex
|
||||
const item = sortedData[dataIndex]
|
||||
return `
|
||||
<div style="padding: 8px;">
|
||||
<div><strong>${params[0].name}</strong></div>
|
||||
<div>连接率: <strong>${item.connectionRate}%</strong></div>
|
||||
<div>总数量: ${item.totalCount}</div>
|
||||
<div>成功数量: ${item.successCount}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: operators,
|
||||
axisLabel: {
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '连接率 (%)',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}%'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '连接率',
|
||||
type: 'bar',
|
||||
data: rates,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 0.5, color: '#188df0' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#2378f7' },
|
||||
{ offset: 0.7, color: '#2378f7' },
|
||||
{ offset: 1, color: '#83bff6' }
|
||||
])
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: '{c}%',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.value.setOption(option)
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const body: { startDate?: string; endDate?: string } = {}
|
||||
if (startDate.value) body.startDate = startDate.value
|
||||
if (endDate.value) body.endDate = endDate.value
|
||||
|
||||
const response = await fetch('http://localhost:8081/dataConnection/connectionRate', {
|
||||
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 ConnectionRateItem[]
|
||||
|
||||
// 输出返回的数据到控制台
|
||||
console.log('返回的数据:', data)
|
||||
|
||||
await nextTick()
|
||||
updateChart(data)
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
// 清空图表
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.setOption({
|
||||
series: [{ data: [] }]
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听窗口大小变化,自动调整图表大小
|
||||
const handleResize = () => {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
initChart()
|
||||
await fetchData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose()
|
||||
chartInstance.value = 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>业务需求16:数据链接-数据链接率统计</h2>
|
||||
|
||||
<div class="filter-bar">
|
||||
<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="chart-container" ref="chartContainer"></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;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-size: 14px;
|
||||
}
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
.refresh-btn:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
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>
|
||||
15
src/views/hot-app/Req17AppTrafficDistributionView.vue
Normal file
15
src/views/hot-app/Req17AppTrafficDistributionView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求17:热门APP-热门APP流量分布</h2>
|
||||
<p>这里展示热门APP流量分布的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-app/Req18AppAnalysisView.vue
Normal file
15
src/views/hot-app/Req18AppAnalysisView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求18:热门APP-热门APP分析</h2>
|
||||
<p>这里展示热门APP分析的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-app/Req19AppTrafficRankingView.vue
Normal file
15
src/views/hot-app/Req19AppTrafficRankingView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求19:热门APP-热门APP流量排名</h2>
|
||||
<p>这里展示热门APP流量排名的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-app/Req20AppTrafficTraceView.vue
Normal file
15
src/views/hot-app/Req20AppTrafficTraceView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求20:热门APP-热门APP流量跟踪</h2>
|
||||
<p>这里展示热门APP流量跟踪的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-app/Req21LandmarkAppTrafficRankingView.vue
Normal file
15
src/views/hot-app/Req21LandmarkAppTrafficRankingView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求21:热门APP-典型地标热门APP流量排名</h2>
|
||||
<p>这里展示典型地标热门APP流量排名的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-phone/Req22PhoneTrafficDistributionView.vue
Normal file
15
src/views/hot-phone/Req22PhoneTrafficDistributionView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求22:热门手机-热门手机流量分布</h2>
|
||||
<p>这里展示热门手机流量分布的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-phone/Req23PhoneQualityRankingView.vue
Normal file
15
src/views/hot-phone/Req23PhoneQualityRankingView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求23:热门手机-热门手机网络质量排名</h2>
|
||||
<p>这里展示热门手机网络质量排名的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-phone/Req24PhoneTrafficRankingView.vue
Normal file
15
src/views/hot-phone/Req24PhoneTrafficRankingView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求24:热门手机-热门手机流量排名</h2>
|
||||
<p>这里展示热门手机流量排名的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-phone/Req25OsTrafficRankingView.vue
Normal file
15
src/views/hot-phone/Req25OsTrafficRankingView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求25:热门手机-手机OS流量排名</h2>
|
||||
<p>这里展示手机OS流量排名的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-phone/Req26PhoneDistributionMapView.vue
Normal file
15
src/views/hot-phone/Req26PhoneDistributionMapView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求26:热门手机-热门手机分布图</h2>
|
||||
<p>这里展示热门手机分布图的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/hot-phone/Req27OsDistributionMapView.vue
Normal file
15
src/views/hot-phone/Req27OsDistributionMapView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求27:热门手机-手机OS分布图</h2>
|
||||
<p>这里展示手机OS分布图的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/network-quality/Req10QualityDistributionView.vue
Normal file
15
src/views/network-quality/Req10QualityDistributionView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求10:网络质量-网络质量分布</h2>
|
||||
<p>这里展示网络质量分布的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/network-quality/Req11QualityStatView.vue
Normal file
15
src/views/network-quality/Req11QualityStatView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求11:网络质量-网络质量统计</h2>
|
||||
<p>这里展示网络质量统计的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/network-quality/Req12SpeedRankingView.vue
Normal file
15
src/views/network-quality/Req12SpeedRankingView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求12:网络质量-网络速率排名</h2>
|
||||
<p>这里展示网络速率排名的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/network-quality/Req13LandmarkQualityTraceView.vue
Normal file
15
src/views/network-quality/Req13LandmarkQualityTraceView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求13:网络质量-典型地标网络质量跟踪</h2>
|
||||
<p>这里展示典型地标网络质量跟踪的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/network-quality/Req14LandmarkQualityStatView.vue
Normal file
15
src/views/network-quality/Req14LandmarkQualityStatView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求14:网络质量-典型地标网络质量统计</h2>
|
||||
<p>这里展示典型地标网络质量统计的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/personal-user/Req30NearbyNetworkQualityView.vue
Normal file
15
src/views/personal-user/Req30NearbyNetworkQualityView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求30:个人用户-周边网络质量</h2>
|
||||
<p>这里展示个人用户周边网络质量的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/personal-user/Req31NearbyHotAppView.vue
Normal file
15
src/views/personal-user/Req31NearbyHotAppView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求31:个人用户-周边热门APP</h2>
|
||||
<p>这里展示个人用户周边热门APP的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/personal-user/Req32NearbyHotOsView.vue
Normal file
15
src/views/personal-user/Req32NearbyHotOsView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求32:个人用户-周边热门OS</h2>
|
||||
<p>这里展示个人用户周边热门OS的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/personal-user/Req33NearbySignalCoverageView.vue
Normal file
15
src/views/personal-user/Req33NearbySignalCoverageView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求33:个人用户-周边信号覆盖</h2>
|
||||
<p>这里展示个人用户周边信号覆盖的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求7:信号覆盖-信号强度分布图</h2>
|
||||
<p>这里展示信号强度分布图的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/signal-coverage/Req8LandmarkStrengthTraceView.vue
Normal file
15
src/views/signal-coverage/Req8LandmarkStrengthTraceView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求8:信号覆盖-典型地标信号强度跟踪</h2>
|
||||
<p>这里展示典型地标信号强度跟踪的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/views/signal-coverage/Req9LandmarkStrengthStatView.vue
Normal file
15
src/views/signal-coverage/Req9LandmarkStrengthStatView.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求9:信号覆盖-典型地标信号强度统计</h2>
|
||||
<p>这里展示典型地标信号强度统计的相关内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user