添加百度地图类型声明,更新 HTML 以加载百度地图 API,新增多个视图组件和路由配置以支持信号覆盖、网络质量、数据连接、热门应用和个人用户的统计展示。

This commit is contained in:
wangran
2026-01-09 16:51:16 +08:00
parent eef27d2bbc
commit a2379ccf2a
42 changed files with 7339 additions and 8 deletions

View File

@@ -1,11 +1,264 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { ref } from 'vue'
type MenuItem = {
key: string
title: string
children: { title: string; path: string }[]
}
const menus = ref<MenuItem[]>([
{
key: 'signal',
title: '信号覆盖',
children: [
{ title: '业务需求7信号覆盖-信号强度分布图', path: '/signal-coverage/req7-strength-distribution' },
{ title: '业务需求8信号覆盖-典型地标信号强度跟踪', path: '/signal-coverage/req8-landmark-strength-trace' },
{ title: '业务需求9信号覆盖-典型地标信号强度统计', path: '/signal-coverage/req9-landmark-strength-stat' },
],
},
{
key: 'network',
title: '网络质量',
children: [
{ title: '业务需求10网络质量-网络质量分布', path: '/network-quality/req10-quality-distribution' },
{ title: '业务需求11网络质量-网络质量统计', path: '/network-quality/req11-quality-stat' },
{ title: '业务需求12网络质量-网络速率排名', path: '/network-quality/req12-speed-ranking' },
{ title: '业务需求13网络质量-典型地标网络质量跟踪', path: '/network-quality/req13-landmark-quality-trace' },
{ title: '业务需求14网络质量-典型地标网络质量统计', path: '/network-quality/req14-landmark-quality-stat' },
],
},
{
key: 'data',
title: '数据连接',
children: [
{ title: '业务需求15数据链接-数据链接率分布', path: '/data-connection/req15-rate-distribution' },
{ title: '业务需求16数据链接-数据链接率统计', path: '/data-connection/req16-rate-stat' },
],
},
{
key: 'hot-app',
title: '热门 App',
children: [
{ title: '业务需求17热门APP-热门APP流量分布', path: '/hot-app/req17-traffic-distribution' },
{ title: '业务需求18热门APP-热门APP分析', path: '/hot-app/req18-analysis' },
{ title: '业务需求19热门APP-热门APP流量排名', path: '/hot-app/req19-traffic-ranking' },
{ title: '业务需求20热门APP-热门APP流量跟踪', path: '/hot-app/req20-traffic-trace' },
{ title: '业务需求21热门APP-典型地标热门APP流量排名', path: '/hot-app/req21-landmark-traffic-ranking' },
],
},
{
key: 'hot-phone',
title: '热门手机',
children: [
{ title: '业务需求22热门手机-热门手机流量分布', path: '/hot-phone/req22-traffic-distribution' },
{ title: '业务需求23热门手机-热门手机网络质量排名', path: '/hot-phone/req23-quality-ranking' },
{ title: '业务需求24热门手机-热门手机流量排名', path: '/hot-phone/req24-traffic-ranking' },
{ title: '业务需求25热门手机-手机OS流量排名', path: '/hot-phone/req25-os-traffic-ranking' },
{ title: '业务需求26热门手机-热门手机分布图', path: '/hot-phone/req26-distribution-map' },
{ title: '业务需求27热门手机-手机OS分布图', path: '/hot-phone/req27-os-distribution-map' },
],
},
{
key: 'connection',
title: '连接点',
children: [
{ title: '业务需求28连接点-连接点排名', path: '/connection-point/req28-ranking' },
{ title: '业务需求29连接点-连接点地理分布', path: '/connection-point/req29-geo-distribution' },
],
},
{
key: 'personal',
title: '个人用户',
children: [
{ title: '业务需求30个人用户-周边网络质量', path: '/personal-user/req30-nearby-network-quality' },
{ title: '业务需求31个人用户-周边热门APP', path: '/personal-user/req31-nearby-hot-app' },
{ title: '业务需求32个人用户-周边热门OS', path: '/personal-user/req32-nearby-hot-os' },
{ title: '业务需求33个人用户-周边信号覆盖', path: '/personal-user/req33-nearby-signal-coverage' },
],
},
])
const expandedKeys = ref<string[]>(['signal', 'network'])
const isExpanded = (key: string) => expandedKeys.value.includes(key)
const toggleExpand = (key: string) => {
if (isExpanded(key)) {
expandedKeys.value = expandedKeys.value.filter((k) => k !== key)
} else {
expandedKeys.value.push(key)
}
}
</script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
<div class="layout">
<aside class="sider">
<div class="logo">
<RouterLink to="/" class="logo-link">
仪表盘
</RouterLink>
</div>
<nav class="nav">
<div
v-for="menu in menus"
:key="menu.key"
class="nav-group"
>
<div
class="nav-item nav-parent"
@click="toggleExpand(menu.key)"
>
<span>{{ menu.title }}</span>
<span class="arrow" :class="{ open: isExpanded(menu.key) }"></span>
</div>
<div v-if="isExpanded(menu.key)" class="sub-nav">
<RouterLink
v-for="child in menu.children"
:key="child.path"
:to="child.path"
class="sub-nav-item"
active-class="active"
>
{{ child.title }}
</RouterLink>
</div>
</div>
</nav>
</aside>
<main class="content">
<header class="header">
<h1 class="title">Vue 示例项目</h1>
</header>
<section class="page-wrapper">
<RouterView />
</section>
</main>
</div>
</template>
<style scoped></style>
<style scoped>
.layout {
display: flex;
height: 100vh;
background: #f5f7fa;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif;
}
.sider {
width: 220px;
background: #1f2933;
color: #fff;
display: flex;
flex-direction: column;
padding: 16px 0;
}
.logo {
font-size: 20px;
font-weight: 600;
padding: 0 24px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.logo-link {
color: #fff;
text-decoration: none;
}
.nav {
margin-top: 12px;
display: flex;
flex-direction: column;
}
.nav-item {
padding: 10px 24px;
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
}
.nav-group + .nav-group {
margin-top: 4px;
}
.nav-parent {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.arrow {
font-size: 12px;
transform: rotate(-90deg);
transition: transform 0.2s;
}
.arrow.open {
transform: rotate(0deg);
}
.sub-nav {
display: flex;
flex-direction: column;
}
.sub-nav-item {
padding: 6px 32px;
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
}
.sub-nav-item:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.sub-nav-item.active {
background: #2563eb;
color: #fff;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.nav-item.active {
background: #2563eb;
color: #fff;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
}
.header {
height: 56px;
display: flex;
align-items: center;
padding: 0 24px;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.title {
margin: 0;
font-size: 18px;
}
.page-wrapper {
flex: 1;
overflow: auto;
}
</style>

View File

@@ -1,8 +1,198 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
// 信号覆盖
import Req7SignalStrengthDistributionView from '../views/signal-coverage/Req7SignalStrengthDistributionView.vue'
import Req8LandmarkStrengthTraceView from '../views/signal-coverage/Req8LandmarkStrengthTraceView.vue'
import Req9LandmarkStrengthStatView from '../views/signal-coverage/Req9LandmarkStrengthStatView.vue'
// 网络质量
import Req10QualityDistributionView from '../views/network-quality/Req10QualityDistributionView.vue'
import Req11QualityStatView from '../views/network-quality/Req11QualityStatView.vue'
import Req12SpeedRankingView from '../views/network-quality/Req12SpeedRankingView.vue'
import Req13LandmarkQualityTraceView from '../views/network-quality/Req13LandmarkQualityTraceView.vue'
import Req14LandmarkQualityStatView from '../views/network-quality/Req14LandmarkQualityStatView.vue'
// 数据连接
import Req15RateDistributionView from '../views/data-connection/Req15RateDistributionView.vue'
import Req16RateStatView from '../views/data-connection/Req16RateStatView.vue'
// 热门 APP
import Req17AppTrafficDistributionView from '../views/hot-app/Req17AppTrafficDistributionView.vue'
import Req18AppAnalysisView from '../views/hot-app/Req18AppAnalysisView.vue'
import Req19AppTrafficRankingView from '../views/hot-app/Req19AppTrafficRankingView.vue'
import Req20AppTrafficTraceView from '../views/hot-app/Req20AppTrafficTraceView.vue'
import Req21LandmarkAppTrafficRankingView from '../views/hot-app/Req21LandmarkAppTrafficRankingView.vue'
// 热门手机
import Req22PhoneTrafficDistributionView from '../views/hot-phone/Req22PhoneTrafficDistributionView.vue'
import Req23PhoneQualityRankingView from '../views/hot-phone/Req23PhoneQualityRankingView.vue'
import Req24PhoneTrafficRankingView from '../views/hot-phone/Req24PhoneTrafficRankingView.vue'
import Req25OsTrafficRankingView from '../views/hot-phone/Req25OsTrafficRankingView.vue'
import Req26PhoneDistributionMapView from '../views/hot-phone/Req26PhoneDistributionMapView.vue'
import Req27OsDistributionMapView from '../views/hot-phone/Req27OsDistributionMapView.vue'
// 连接点
import Req28ConnectionRankingView from '../views/connection-point/Req28ConnectionRankingView.vue'
import Req29ConnectionGeoDistributionView from '../views/connection-point/Req29ConnectionGeoDistributionView.vue'
// 个人用户
import Req30NearbyNetworkQualityView from '../views/personal-user/Req30NearbyNetworkQualityView.vue'
import Req31NearbyHotAppView from '../views/personal-user/Req31NearbyHotAppView.vue'
import Req32NearbyHotOsView from '../views/personal-user/Req32NearbyHotOsView.vue'
import Req33NearbySignalCoverageView from '../views/personal-user/Req33NearbySignalCoverageView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
routes: [
{
path: '/',
name: 'dashboard',
component: HomeView,
},
// 信号覆盖 7-9
{
path: '/signal-coverage/req7-strength-distribution',
name: 'req7-signal-strength-distribution',
component: Req7SignalStrengthDistributionView,
},
{
path: '/signal-coverage/req8-landmark-strength-trace',
name: 'req8-landmark-strength-trace',
component: Req8LandmarkStrengthTraceView,
},
{
path: '/signal-coverage/req9-landmark-strength-stat',
name: 'req9-landmark-strength-stat',
component: Req9LandmarkStrengthStatView,
},
// 网络质量 10-14
{
path: '/network-quality/req10-quality-distribution',
name: 'req10-quality-distribution',
component: Req10QualityDistributionView,
},
{
path: '/network-quality/req11-quality-stat',
name: 'req11-quality-stat',
component: Req11QualityStatView,
},
{
path: '/network-quality/req12-speed-ranking',
name: 'req12-speed-ranking',
component: Req12SpeedRankingView,
},
{
path: '/network-quality/req13-landmark-quality-trace',
name: 'req13-landmark-quality-trace',
component: Req13LandmarkQualityTraceView,
},
{
path: '/network-quality/req14-landmark-quality-stat',
name: 'req14-landmark-quality-stat',
component: Req14LandmarkQualityStatView,
},
// 数据连接 15-16
{
path: '/data-connection/req15-rate-distribution',
name: 'req15-rate-distribution',
component: Req15RateDistributionView,
},
{
path: '/data-connection/req16-rate-stat',
name: 'req16-rate-stat',
component: Req16RateStatView,
},
// 热门 APP 17-21
{
path: '/hot-app/req17-traffic-distribution',
name: 'req17-app-traffic-distribution',
component: Req17AppTrafficDistributionView,
},
{
path: '/hot-app/req18-analysis',
name: 'req18-app-analysis',
component: Req18AppAnalysisView,
},
{
path: '/hot-app/req19-traffic-ranking',
name: 'req19-app-traffic-ranking',
component: Req19AppTrafficRankingView,
},
{
path: '/hot-app/req20-traffic-trace',
name: 'req20-app-traffic-trace',
component: Req20AppTrafficTraceView,
},
{
path: '/hot-app/req21-landmark-traffic-ranking',
name: 'req21-app-landmark-traffic-ranking',
component: Req21LandmarkAppTrafficRankingView,
},
// 热门手机 22-27
{
path: '/hot-phone/req22-traffic-distribution',
name: 'req22-phone-traffic-distribution',
component: Req22PhoneTrafficDistributionView,
},
{
path: '/hot-phone/req23-quality-ranking',
name: 'req23-phone-quality-ranking',
component: Req23PhoneQualityRankingView,
},
{
path: '/hot-phone/req24-traffic-ranking',
name: 'req24-phone-traffic-ranking',
component: Req24PhoneTrafficRankingView,
},
{
path: '/hot-phone/req25-os-traffic-ranking',
name: 'req25-os-traffic-ranking',
component: Req25OsTrafficRankingView,
},
{
path: '/hot-phone/req26-distribution-map',
name: 'req26-phone-distribution-map',
component: Req26PhoneDistributionMapView,
},
{
path: '/hot-phone/req27-os-distribution-map',
name: 'req27-os-distribution-map',
component: Req27OsDistributionMapView,
},
// 连接点 28-29
{
path: '/connection-point/req28-ranking',
name: 'req28-connection-ranking',
component: Req28ConnectionRankingView,
},
{
path: '/connection-point/req29-geo-distribution',
name: 'req29-connection-geo-distribution',
component: Req29ConnectionGeoDistributionView,
},
// 个人用户 30-33
{
path: '/personal-user/req30-nearby-network-quality',
name: 'req30-nearby-network-quality',
component: Req30NearbyNetworkQualityView,
},
{
path: '/personal-user/req31-nearby-hot-app',
name: 'req31-nearby-hot-app',
component: Req31NearbyHotAppView,
},
{
path: '/personal-user/req32-nearby-hot-os',
name: 'req32-nearby-hot-os',
component: Req32NearbyHotOsView,
},
{
path: '/personal-user/req33-nearby-signal-coverage',
name: 'req33-nearby-signal-coverage',
component: Req33NearbySignalCoverageView,
},
],
})
export default router

15
src/views/AboutView.vue Normal file
View 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>

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

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