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

385 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 operatorColorMap: Record<string, string> = {
CMCC: '#ff8a48', // 橙色
CUCC: '#4b9cff', // 蓝色
CTCC: '#c572ff' // 紫色
}
// 初始化图表
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: (params: any) => {
const item = sortedData[params.dataIndex]
return operatorColorMap[item.operator] || '#83bff6'
}
},
emphasis: {
itemStyle: {
opacity: 0.9
}
},
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="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>
<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;
}
.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;
}
.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>