lqq修改
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=4CSApBVQYDpCbrRQxu86FOOwbFjVjDlb"></script>
|
||||
<script type="text/javascript" src="//api.map.baidu.com/library/Heatmap/2.0/src/Heatmap_min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
773
package-lock.json
generated
773
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.26",
|
||||
"vue-baidu-map-3x": "^1.0.40",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,15 +1,302 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
|
||||
|
||||
// 信号强度数据项接口
|
||||
interface SignalStrengthItem {
|
||||
id?: number
|
||||
userLon?: string
|
||||
userLat?: string
|
||||
rssi?: number // 确保 rssi 是 number
|
||||
[key: string]: any // 允许其他字段
|
||||
}
|
||||
|
||||
const mapContainer = ref<HTMLDivElement | null>(null)
|
||||
const networkType = ref<string>('ALL')
|
||||
const networkName = ref<string>('ALL')
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
const dataLoaded = ref<boolean>(false)
|
||||
|
||||
let map: any = null
|
||||
let heatmapOverlay: any = null
|
||||
|
||||
// 将后端返回的数据转换为热力图库需要的格式
|
||||
const transformDataForHeatmap = (data: SignalStrengthItem[]) => {
|
||||
return data
|
||||
.filter(item => {
|
||||
const lat = parseFloat(String(item.userLat))
|
||||
const lon = parseFloat(String(item.userLon))
|
||||
// 过滤掉无效坐标和无效信号强度
|
||||
return !isNaN(lat) && !isNaN(lon) && lat !== 0 && lon !== 0 && item.rssi != null
|
||||
})
|
||||
.map(item => {
|
||||
// 热力图权重需要是正数,我们将RSSI(-120到0)映射到一个正数范围(例如1到100)
|
||||
// 信号越强(越接近0),权重越高
|
||||
const rssi = item.rssi ?? -120;
|
||||
const count = Math.max(1, 100 - (Math.abs(rssi) - 20));
|
||||
return {
|
||||
lng: parseFloat(String(item.userLon)),
|
||||
lat: parseFloat(String(item.userLat)),
|
||||
count: count
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染热力图
|
||||
const renderHeatmap = (points: any[]) => {
|
||||
if (!map) return
|
||||
|
||||
// 如果已存在热力图层,先移除
|
||||
if (heatmapOverlay) {
|
||||
map.removeOverlay(heatmapOverlay)
|
||||
}
|
||||
|
||||
if (points.length === 0) {
|
||||
return; // 没有数据点,不创建图层
|
||||
}
|
||||
|
||||
// 创建热力图实例
|
||||
heatmapOverlay = new window.BMapLib.HeatmapOverlay({ radius: 20, opacity: 0.8 })
|
||||
map.addOverlay(heatmapOverlay)
|
||||
|
||||
// 设置热力图数据
|
||||
heatmapOverlay.setDataSet({
|
||||
data: points,
|
||||
max: 100 // 设置权重的最大值
|
||||
});
|
||||
|
||||
// 自动调整地图视野以包含所有点
|
||||
const viewport = map.getViewport(points.map((p: any) => new window.BMap.Point(p.lng, p.lat)));
|
||||
map.centerAndZoom(viewport.center, viewport.zoom);
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
dataLoaded.value = false
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.append('networkType', networkType.value)
|
||||
params.append('networkName', networkName.value)
|
||||
if (startDate.value) params.append('startTime', new Date(startDate.value).getTime().toString())
|
||||
if (endDate.value) {
|
||||
const end = new Date(endDate.value);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
params.append('endTime', end.getTime().toString());
|
||||
}
|
||||
|
||||
const response = await fetch(`http://localhost:8081/api/signal/heatmap?${params.toString()}`)
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.code !== 200) throw new Error(responseData.message || '获取数据失败')
|
||||
|
||||
const heatmapPoints = transformDataForHeatmap(responseData.data || [])
|
||||
|
||||
if (map) {
|
||||
renderHeatmap(heatmapPoints)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
if(map) renderHeatmap([]); // 清空热力图
|
||||
} finally {
|
||||
loading.value = false
|
||||
dataLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
const initMap = async () => {
|
||||
await nextTick()
|
||||
|
||||
// 轮询检查百度地图API和热力图库是否都已加载
|
||||
if (!window.BMap || !window.BMapLib?.HeatmapOverlay) {
|
||||
console.warn('百度地图API或热力图库未加载,200ms后重试...')
|
||||
setTimeout(initMap, 200)
|
||||
return
|
||||
}
|
||||
|
||||
if (!mapContainer.value) {
|
||||
console.warn('地图容器未找到')
|
||||
return
|
||||
}
|
||||
|
||||
map = new window.BMap.Map(mapContainer.value)
|
||||
map.centerAndZoom(new window.BMap.Point(105.8951, 36.5667), 5) // 默认中国中心
|
||||
map.enableScrollWheelZoom(true)
|
||||
map.addControl(new window.BMap.MapTypeControl({ mapTypes: [window.BMap.MapTypeId.NORMAL, window.BMap.MapTypeId.SATELLITE] }))
|
||||
map.addControl(new window.BMap.NavigationControl())
|
||||
|
||||
// 地图初始化后立即获取数据
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([networkType, networkName, startDate, endDate], () => {
|
||||
if (map && dataLoaded.value) {
|
||||
fetchData()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initMap()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (heatmapOverlay && map) {
|
||||
map.removeOverlay(heatmapOverlay)
|
||||
}
|
||||
map = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求7:信号覆盖-信号强度分布图</h2>
|
||||
<p>这里展示信号强度分布图的相关内容。</p>
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p class="loading-text">加载数据中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>业务需求7:信号覆盖-信号强度分布(热力图)</h2>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label for="networkType">运营商:</label>
|
||||
<select id="networkType" v-model="networkType" class="select-input" :disabled="loading">
|
||||
<option value="ALL">ALL</option>
|
||||
<option value="CMCC">中国移动</option>
|
||||
<option value="CUCC">中国联通</option>
|
||||
<option value="CTCC">中国电信</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="networkName">网络类型:</label>
|
||||
<select id="networkName" v-model="networkName" class="select-input" :disabled="loading">
|
||||
<option value="ALL">ALL</option>
|
||||
<option value="2G">2G</option>
|
||||
<option value="3G">3G</option>
|
||||
<option value="4G">4G</option>
|
||||
<option value="5G">5G</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="startDate">起始日期:</label>
|
||||
<input id="startDate" type="date" v-model="startDate" class="date-input" :disabled="loading" />
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="endDate">结束日期:</label>
|
||||
<input id="endDate" type="date" v-model="endDate" class="date-input" :disabled="loading" />
|
||||
</div>
|
||||
<button @click="fetchData" class="refresh-btn" :disabled="loading">
|
||||
{{ loading ? '加载中...' : '刷新' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="map-container" ref="mapContainer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
h2 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.select-input,
|
||||
.date-input {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 6px 16px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
.refresh-btn:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 220px);
|
||||
min-height: 500px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,556 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2>业务需求8:信号覆盖-典型地标信号强度跟踪</h2>
|
||||
<p>这里展示典型地标信号强度跟踪的相关内容。</p>
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p class="loading-text">加载数据中...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label for="landmark">地标名称:</label>
|
||||
<select
|
||||
id="landmark"
|
||||
v-model="landmark"
|
||||
class="select-input"
|
||||
:disabled="loading"
|
||||
@change="fetchData"
|
||||
>
|
||||
<option value="">请选择地标</option>
|
||||
<option v-for="item in landmarkOptions" :key="item" :value="item">
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="loadLandmarks" :disabled="loading" class="refresh-btn">
|
||||
刷新地标列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="networkType">网络制式:</label>
|
||||
<select
|
||||
id="networkType"
|
||||
v-model="networkType"
|
||||
class="select-input"
|
||||
:disabled="loading"
|
||||
@change="fetchData"
|
||||
>
|
||||
<option value="ALL">ALL</option>
|
||||
<option value="2G">2G</option>
|
||||
<option value="3G">3G</option>
|
||||
<option value="4G">4G</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="timeGranularity">时间粒度:</label>
|
||||
<select
|
||||
id="timeGranularity"
|
||||
v-model="timeGranularity"
|
||||
class="select-input"
|
||||
:disabled="loading"
|
||||
@change="fetchData"
|
||||
>
|
||||
<option value="hour">按小时</option>
|
||||
<option value="day" selected>按天</option>
|
||||
<option value="month">按月</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="userRole">用户角色:</label>
|
||||
<select
|
||||
id="userRole"
|
||||
v-model="userRole"
|
||||
class="select-input"
|
||||
:disabled="loading"
|
||||
@change="fetchData"
|
||||
>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="user_CMCC">中国移动用户</option>
|
||||
<option value="user_CUCC">中国联通用户</option>
|
||||
<option value="user_CTCC">中国电信用户</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="startTime">开始时间:</label>
|
||||
<input
|
||||
id="startTime"
|
||||
type="text"
|
||||
v-model="startTime"
|
||||
class="date-input"
|
||||
:disabled="loading"
|
||||
placeholder="格式: 2023070100"
|
||||
@change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="endTime">结束时间:</label>
|
||||
<input
|
||||
id="endTime"
|
||||
type="text"
|
||||
v-model="endTime"
|
||||
class="date-input"
|
||||
:disabled="loading"
|
||||
placeholder="格式: 2023073123"
|
||||
@change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="statistics" class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">数据点数:</span>
|
||||
<span class="stat-value">{{ statistics.totalDataPoints }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">时间周期数:</span>
|
||||
<span class="stat-value">{{ statistics.timePeriods }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">运营商数:</span>
|
||||
<span class="stat-value">{{ statistics.operatorCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" ref="chartContainer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onUnmounted, nextTick, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 接口返回的图表数据结构
|
||||
interface LinechartData {
|
||||
timeLabels: string[]
|
||||
series: Array<{
|
||||
name: string
|
||||
type: string
|
||||
data: (number | null)[]
|
||||
smooth?: boolean
|
||||
symbol?: string
|
||||
symbolSize?: number
|
||||
}>
|
||||
operators: string[]
|
||||
landmark: string
|
||||
networkType: string
|
||||
timeGranularity: string
|
||||
statistics: {
|
||||
totalDataPoints: number
|
||||
timePeriods: number
|
||||
operatorCount: number
|
||||
}
|
||||
}
|
||||
|
||||
const chartContainer = ref<HTMLDivElement | null>(null)
|
||||
const landmark = ref<string>('大学') // 默认地标
|
||||
const networkType = ref<string>('ALL')
|
||||
const timeGranularity = ref<string>('day')
|
||||
const userRole = ref<string>('admin')
|
||||
const startTime = ref<string>('')
|
||||
const endTime = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
const landmarkOptions = ref<string[]>([])
|
||||
const statistics = ref<any>(null)
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 运营商颜色映射
|
||||
const operatorColorMap: Record<string, string> = {
|
||||
'CMCC': '#ff8a48', // 中国移动 - 橙色
|
||||
'CUCC': '#4b9cff', // 中国联通 - 蓝色
|
||||
'CTCC': '#c572ff' // 中国电信 - 紫色
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = (data: LinechartData) => {
|
||||
if (!chartInstance || !data || !data.timeLabels || !data.series) return
|
||||
|
||||
const calculateLabelInterval = (length: number) => {
|
||||
if (length <= 10) return 0
|
||||
if (length <= 30) return 1
|
||||
if (length <= 60) return 3
|
||||
return Math.ceil(length / 20) // 最多显示20个标签
|
||||
}
|
||||
|
||||
const labelInterval = calculateLabelInterval(data.timeLabels.length)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: `${data.landmark || '典型地标'} - 信号强度跟踪`,
|
||||
subtext: `网络制式: ${data.networkType === 'ALL' ? '全部' : data.networkType} | 时间粒度: ${formatGranularityText(data.timeGranularity)}`,
|
||||
left: 'center'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '12%', // 增加底部边距
|
||||
top: '20%', // 调整顶部边距
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.timeLabels,
|
||||
axisLabel: {
|
||||
interval: labelInterval, // 动态计算标签间隔
|
||||
rotate: 45,
|
||||
margin: 12, // 增加标签与轴线的距离
|
||||
formatter: function(value: string) {
|
||||
if (data.timeGranularity === 'month') {
|
||||
return value
|
||||
} else if (data.timeGranularity === 'day') {
|
||||
return value
|
||||
} else {
|
||||
// 对于小时数据,只显示日期和小时
|
||||
return value.length > 10 ? value.substring(5, 16) : value
|
||||
}
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true // 刻度线与标签对齐
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '信号强度(dBm)',
|
||||
min: -120,
|
||||
max: 0,
|
||||
axisLabel: {
|
||||
formatter: '{value} dBm'
|
||||
}
|
||||
},
|
||||
series: data.series.map(s => ({
|
||||
...s,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
connectNulls: true, // 连接空值,解决线段不连续问题
|
||||
itemStyle: {
|
||||
color: operatorColorMap[s.name] || '#5470c6'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2 // 稍微减小线宽
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#000'
|
||||
}
|
||||
}
|
||||
})),
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: 0,
|
||||
filterMode: 'filter'
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
xAxisIndex: 0,
|
||||
filterMode: 'filter',
|
||||
bottom: 40, // 增加底部距离
|
||||
height: 20,
|
||||
handleSize: '80%' // 调整滑块大小
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
let result = params[0].name + '<br/>'
|
||||
params.forEach((param: any) => {
|
||||
const value = param.value !== null ?
|
||||
param.value.toFixed(2) + ' dBm' : '无数据'
|
||||
result += `
|
||||
<div style="display: flex; align-items: center; margin: 2px 0;">
|
||||
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${param.color}"></span>
|
||||
<span>${param.seriesName}: <strong>${value}</strong></span>
|
||||
</div>`
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
// 添加图例配置
|
||||
legend: {
|
||||
data: data.operators || data.series.map(s => s.name),
|
||||
top: 50,
|
||||
type: 'scroll', // 图例过多时可滚动
|
||||
pageIconSize: 12,
|
||||
pageTextStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用配置
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
// 格式化时间粒度文本
|
||||
const formatGranularityText = (granularity: string) => {
|
||||
switch (granularity) {
|
||||
case 'month': return '按月'
|
||||
case 'day': return '按天'
|
||||
case 'hour': return '按小时'
|
||||
default: return granularity
|
||||
}
|
||||
}
|
||||
|
||||
// 加载地标列表
|
||||
const loadLandmarks = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8081/api/signal/landmarks')
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.code === 200 && responseData.data) {
|
||||
landmarkOptions.value = responseData.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载地标列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
if (!landmark.value) {
|
||||
console.error('请先选择地标')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.append('landmark', landmark.value)
|
||||
params.append('networkType', networkType.value)
|
||||
params.append('timeGranularity', timeGranularity.value)
|
||||
params.append('userRole', userRole.value)
|
||||
|
||||
if (startTime.value) {
|
||||
params.append('startTime', startTime.value)
|
||||
}
|
||||
if (endTime.value) {
|
||||
params.append('endTime', endTime.value)
|
||||
}
|
||||
|
||||
const response = await fetch(`http://localhost:8081/api/signal/linechart?${params.toString()}`)
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.code !== 200) throw new Error(responseData.message || '获取数据失败')
|
||||
|
||||
const data = responseData.data
|
||||
statistics.value = data.statistics
|
||||
|
||||
if (data && data.timeLabels && data.series) {
|
||||
updateChart(data)
|
||||
} else {
|
||||
throw new Error('数据格式错误')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
if (chartInstance) {
|
||||
chartInstance.clear()
|
||||
}
|
||||
console.error('获取数据失败: ' + (error as Error).message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([landmark, networkType, timeGranularity, userRole, startTime, endTime], (newVal, oldVal) => {
|
||||
// 避免初始加载时重复调用
|
||||
if (newVal.some((val, index) => val !== oldVal[index])) {
|
||||
fetchData()
|
||||
}
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
loadLandmarks()
|
||||
// 延迟一点加载数据,确保地标列表先加载
|
||||
setTimeout(() => {
|
||||
if (landmarkOptions.value.length > 0) {
|
||||
fetchData()
|
||||
}
|
||||
}, 500)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
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;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-item label {
|
||||
min-width: 70px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.select-input,
|
||||
.date-input {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
background-color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
min-height: 500px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dbeafe;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -1,15 +1,274 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onUnmounted, nextTick, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 接口返回的图表数据结构
|
||||
interface BarchartData {
|
||||
landmarks: string[]
|
||||
series: Array<{
|
||||
name: string
|
||||
type: string
|
||||
data: (number | null)[]
|
||||
}>
|
||||
}
|
||||
|
||||
const chartContainer = ref<HTMLDivElement | null>(null)
|
||||
const networkType = ref<string>('ALL')
|
||||
const userRole = ref<string>('admin') // 新增用户角色状态
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 网络类型颜色映射
|
||||
const networkColorMap: Record<string, string> = {
|
||||
'2G': '#ff8a48',
|
||||
'3G': '#4b9cff',
|
||||
'4G': '#c572ff'
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = (data: BarchartData) => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '典型地标信号强度统计',
|
||||
subtext: '单位(dBm)',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: data.series.map(s => s.name),
|
||||
top: 50,
|
||||
left: 'center'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
top: '25%'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.landmarks,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: -120,
|
||||
max: 0,
|
||||
axisLabel: {
|
||||
formatter: '{value} dBm'
|
||||
}
|
||||
},
|
||||
series: data.series.map(s => ({
|
||||
...s,
|
||||
itemStyle: {
|
||||
color: networkColorMap[s.name] || '#5470c6'
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.append('networkType', networkType.value)
|
||||
params.append('userRole', userRole.value) // 添加userRole参数
|
||||
|
||||
if (startDate.value) {
|
||||
params.append('startTime', new Date(startDate.value).getTime().toString())
|
||||
}
|
||||
if (endDate.value) {
|
||||
const end = new Date(endDate.value)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
params.append('endTime', end.getTime().toString())
|
||||
}
|
||||
|
||||
const response = await fetch(`http://localhost:8081/api/signal/barchart?${params.toString()}`)
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.code !== 200) throw new Error(responseData.message || '获取数据失败')
|
||||
|
||||
updateChart(responseData.data || { landmarks: [], series: [] })
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
if (chartInstance) {
|
||||
chartInstance.clear()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([networkType, userRole, startDate, endDate], fetchData)
|
||||
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
fetchData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance?.dispose()
|
||||
})
|
||||
</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>业务需求9:信号覆盖-典型地标信号强度统计</h2>
|
||||
<p>这里展示典型地标信号强度统计的相关内容。</p>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label for="networkType">网络制式:</label>
|
||||
<select id="networkType" v-model="networkType" class="select-input" :disabled="loading">
|
||||
<option value="ALL">ALL</option>
|
||||
<option value="2G">2G</option>
|
||||
<option value="3G">3G</option>
|
||||
<option value="4G">4G</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="userRole">用户角色:</label>
|
||||
<select id="userRole" v-model="userRole" class="select-input" :disabled="loading">
|
||||
<option value="admin">管理员</option>
|
||||
<option value="user_CMCC">中国移动用户</option>
|
||||
<option value="user_CUCC">中国联通用户</option>
|
||||
<option value="user_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>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" ref="chartContainer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user