2026-01-09 16:51:16 +08:00
|
|
|
|
<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: '中国电信'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 16:29:04 +08:00
|
|
|
|
// 运营商颜色映射(与上方图例颜色保持一致)
|
|
|
|
|
|
const operatorColorMap: Record<string, string> = {
|
|
|
|
|
|
CMCC: '#ff8a48', // 橙色
|
|
|
|
|
|
CUCC: '#4b9cff', // 蓝色
|
|
|
|
|
|
CTCC: '#c572ff' // 紫色
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 16:51:16 +08:00
|
|
|
|
// 初始化图表
|
|
|
|
|
|
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: {
|
2026-01-10 16:29:04 +08:00
|
|
|
|
color: (params: any) => {
|
|
|
|
|
|
const item = sortedData[params.dataIndex]
|
|
|
|
|
|
return operatorColorMap[item.operator] || '#83bff6'
|
|
|
|
|
|
}
|
2026-01-09 16:51:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
emphasis: {
|
|
|
|
|
|
itemStyle: {
|
2026-01-10 16:29:04 +08:00
|
|
|
|
opacity: 0.9
|
2026-01-09 16:51:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
2026-01-10 16:29:04 +08:00
|
|
|
|
<!-- 运营商图例标签 -->
|
|
|
|
|
|
<div class="operator-legend">
|
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
|
<span class="legend-color legend-cmcc"></span>
|
|
|
|
|
|
<span class="legend-text">CMCC</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
|
<span class="legend-color legend-cucc"></span>
|
|
|
|
|
|
<span class="legend-text">CUCC</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
|
<span class="legend-color legend-ctcc"></span>
|
|
|
|
|
|
<span class="legend-text">CTCC</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-09 16:51:16 +08:00
|
|
|
|
<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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 16:29:04 +08:00
|
|
|
|
.operator-legend {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
padding: 4px 12px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #4b5563;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-color {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-cmcc {
|
|
|
|
|
|
background-color: #ff8a48;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-cucc {
|
|
|
|
|
|
background-color: #4b9cff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-ctcc {
|
|
|
|
|
|
background-color: #c572ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 16:51:16 +08:00
|
|
|
|
.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>
|