565 lines
13 KiB
Vue
565 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { computed, watch, ref, onUnmounted } from 'vue';
|
|
import { useEcharts } from '@/hooks/common/echarts';
|
|
import { useAppStore } from '@/store/modules/app';
|
|
import type { ECOption } from '@/hooks/common/echarts';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useAuthStore } from '@/store/modules/auth';
|
|
import { clientAuth } from '@/service/ue/client';
|
|
|
|
const { t } = useI18n();
|
|
const authStore = useAuthStore();
|
|
defineOptions({
|
|
name: 'HeaderBanner'
|
|
});
|
|
|
|
const appStore = useAppStore();
|
|
|
|
// 本地界面显示用的数据类型
|
|
interface GaugeDisplayData {
|
|
id: number;
|
|
title: string;
|
|
value: number;
|
|
max: number;
|
|
unit: string;
|
|
displayValue?: string;
|
|
subTitle?: string;
|
|
description?: string;
|
|
}
|
|
|
|
// 添加速率单位转换函数
|
|
const formatSpeed = (speedBps: number): { value: number; unit: string } => {
|
|
// 处理 0 值、undefined 或 null 的情况
|
|
if (!speedBps || speedBps === 0) {
|
|
return { value: 0, unit: 'B/s' };
|
|
}
|
|
|
|
if (speedBps < 1024) {
|
|
return { value: Number(speedBps.toFixed(2)), unit: 'B/s' };
|
|
} else if (speedBps < 1024 * 1024) {
|
|
return { value: Number((speedBps / 1024).toFixed(2)), unit: 'KB/s' };
|
|
} else if (speedBps < 1024 * 1024 * 1024) {
|
|
return { value: Number((speedBps / (1024 * 1024)).toFixed(2)), unit: 'MB/s' };
|
|
} else {
|
|
return { value: Number((speedBps / (1024 * 1024 * 1024)).toFixed(2)), unit: 'GB/s' };
|
|
}
|
|
};
|
|
|
|
// 添加流量单位转换函数
|
|
const formatTraffic = (bytes: number): { value: number; unit: string } => {
|
|
// 处理 0 值、undefined 或 null 的情况
|
|
if (!bytes || bytes === 0) {
|
|
return { value: 0, unit: 'B' };
|
|
}
|
|
|
|
if (bytes < 1024) {
|
|
return { value: Number(bytes.toFixed(2)), unit: 'B' };
|
|
} else if (bytes < 1024 * 1024) {
|
|
return { value: Number((bytes / 1024).toFixed(2)), unit: 'KB' };
|
|
} else if (bytes < 1024 * 1024 * 1024) {
|
|
return { value: Number((bytes / (1024 * 1024)).toFixed(2)), unit: 'MB' };
|
|
} else {
|
|
return { value: Number((bytes / (1024 * 1024 * 1024)).toFixed(2)), unit: 'GB' };
|
|
}
|
|
};
|
|
|
|
// 使用 ref 来存储基础数据
|
|
const baseData = ref<GaugeDisplayData[]>([
|
|
{
|
|
id: 0,
|
|
title: t('page.headerbanner.Remainingcredit'),
|
|
value: 0,
|
|
max: 100,
|
|
unit: '元',
|
|
description: t('page.headerbanner.monthphonebill'),
|
|
subTitle: t('page.headerbanner.deviceCount') + ': 0台'
|
|
},
|
|
{
|
|
id: 1,
|
|
title: t('page.headerbanner.Flowremaining'),
|
|
value: 0,
|
|
max: 1024,
|
|
unit: 'B',
|
|
description: t('page.headerbanner.monthflowr'),
|
|
subTitle: t('page.headerbanner.Used') + ': 0B'
|
|
},
|
|
{
|
|
id: 2,
|
|
title: t('page.headerbanner.Trafficrate'),
|
|
value: 0,
|
|
max: 10,
|
|
unit: 'B/s',
|
|
description: t('page.headerbanner.Currentspeed'),
|
|
subTitle: t('page.headerbanner.maxspeed') + '0B/s'
|
|
}
|
|
]);
|
|
|
|
// 计算属性保持不变
|
|
const gaugeData = computed(() => baseData.value);
|
|
|
|
// 仪盘配置生成函数
|
|
const getGaugeOptions = (data: GaugeDisplayData): ECOption => ({
|
|
backgroundColor: 'transparent',
|
|
series: [{
|
|
name: data.title,
|
|
type: 'gauge',
|
|
min: 0,
|
|
max: data.max,
|
|
startAngle: 200,
|
|
endAngle: -20,
|
|
radius: '85%',
|
|
center: ['50%', '55%'],
|
|
axisLine: {
|
|
lineStyle: {
|
|
width: 15,
|
|
color: [
|
|
[(data.value / data.max), '#4284f5'],
|
|
[1, '#0c47a7']
|
|
]
|
|
}
|
|
},
|
|
pointer: {
|
|
width: 3,
|
|
length: '60%',
|
|
itemStyle: {
|
|
color: '#ffeb3b'
|
|
}
|
|
},
|
|
axisTick: {
|
|
show: true,
|
|
distance: -23,
|
|
length: 8,
|
|
lineStyle: {
|
|
color: '#fff',
|
|
width: 1,
|
|
opacity: 0.3
|
|
}
|
|
},
|
|
splitLine: {
|
|
show: true,
|
|
distance: -22,
|
|
length: 12,
|
|
lineStyle: {
|
|
color: '#fff',
|
|
width: 2,
|
|
opacity: 0.3
|
|
}
|
|
},
|
|
axisLabel: {
|
|
show: false
|
|
},
|
|
detail: {
|
|
show: false
|
|
},
|
|
title: {
|
|
show: false
|
|
},
|
|
data: [{
|
|
value: data.value,
|
|
name: data.title
|
|
}]
|
|
}]
|
|
});
|
|
|
|
// 创建三个仪表盘实例
|
|
const { domRef: gauge1Ref, updateOptions: updateGauge1 } = useEcharts(() => getGaugeOptions(gaugeData.value[0]), {
|
|
onRender: () => {}
|
|
});
|
|
const { domRef: gauge2Ref, updateOptions: updateGauge2 } = useEcharts(() => getGaugeOptions(gaugeData.value[1]), {
|
|
onRender: () => {}
|
|
});
|
|
const { domRef: gauge3Ref, updateOptions: updateGauge3 } = useEcharts(() => getGaugeOptions(gaugeData.value[2]), {
|
|
onRender: () => {}
|
|
});
|
|
|
|
// 更新单个图表的数据
|
|
const updateGaugeData = (opts: ECOption, data: GaugeDisplayData, progressRatio?: number) => {
|
|
// 创建完整的新配置
|
|
const newOpts: ECOption = {
|
|
backgroundColor: 'transparent',
|
|
series: [{
|
|
name: data.title,
|
|
type: 'gauge',
|
|
min: 0,
|
|
max: data.max,
|
|
startAngle: 200,
|
|
endAngle: -20,
|
|
radius: '85%',
|
|
center: ['50%', '55%'],
|
|
axisLine: {
|
|
lineStyle: {
|
|
width: 15,
|
|
color: [
|
|
[(progressRatio !== undefined ? progressRatio : data.value / data.max), '#4284f5'],
|
|
[1, '#0c47a7']
|
|
]
|
|
}
|
|
},
|
|
pointer: {
|
|
width: 3,
|
|
length: '60%',
|
|
itemStyle: {
|
|
color: '#ffeb3b'
|
|
}
|
|
},
|
|
axisTick: {
|
|
show: true,
|
|
distance: -23,
|
|
length: 8,
|
|
lineStyle: {
|
|
color: '#fff',
|
|
width: 1,
|
|
opacity: 0.3
|
|
}
|
|
},
|
|
splitLine: {
|
|
show: true,
|
|
distance: -22,
|
|
length: 12,
|
|
lineStyle: {
|
|
color: '#fff',
|
|
width: 2,
|
|
opacity: 0.3
|
|
}
|
|
},
|
|
axisLabel: {
|
|
show: false
|
|
},
|
|
detail: {
|
|
show: false
|
|
},
|
|
title: {
|
|
show: false
|
|
},
|
|
data: [{
|
|
value: data.value,
|
|
name: data.title
|
|
}]
|
|
}]
|
|
};
|
|
|
|
return newOpts;
|
|
};
|
|
|
|
// 添加峰值速率的 ref
|
|
const peakTrafficRate = ref(0);
|
|
|
|
// 添加检查是否可以上网的函数
|
|
function checkAndTriggerAuth(dashboardData: any) {
|
|
// 检查是否有余额
|
|
const hasBalance = dashboardData.balance > 0;
|
|
|
|
// 检查是否不限制流量
|
|
const noTrafficLimit = !dashboardData.trafficEnable;
|
|
|
|
// 检查是否有剩余流量
|
|
const hasRemainingTraffic = dashboardData.trafficEnable &&
|
|
(dashboardData.traffic - dashboardData.trafficUsed) > 0;
|
|
|
|
// 如果满足任一条件,触发上网
|
|
if (hasBalance || noTrafficLimit || hasRemainingTraffic) {
|
|
clientAuth().then((res) => {
|
|
console.log('Auto auth triggered:', res);
|
|
}).catch(err => {
|
|
console.error('Auto auth failed:', err);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 修改 mockDataUpdate 函数,在获取数据后添加检查
|
|
async function mockDataUpdate() {
|
|
try {
|
|
const response = await authStore.getDashboardData();
|
|
if (response) {
|
|
// 更新余额和设备数据
|
|
baseData.value[0] = {
|
|
...baseData.value[0],
|
|
value: response.balance,
|
|
max: Math.max(response.balance, 100),
|
|
subTitle: t('page.headerbanner.deviceCount') + `: ${response.clientNum}台`
|
|
};
|
|
|
|
// 先计算剩余流量(字节单位)
|
|
const totalTraffic = response.traffic; // 总流量(字节)
|
|
const usedTraffic = response.trafficUsed; // 已用流量(字节)
|
|
|
|
// 计算剩余流量,确保不会出现负数
|
|
const remainingTraffic = Math.max(0, totalTraffic - usedTraffic); // 剩余流量(字节)
|
|
|
|
// 计算进度比例,确保在 0-1 之间
|
|
const progressRatio = Math.min(1, Math.max(0, remainingTraffic / totalTraffic));
|
|
|
|
// 格式化流量显示
|
|
const formattedTotal = formatTraffic(totalTraffic);
|
|
const formattedUsed = formatTraffic(usedTraffic);
|
|
const formattedRemaining = formatTraffic(remainingTraffic);
|
|
|
|
// 更新流量数据显示
|
|
baseData.value[1] = {
|
|
...baseData.value[1],
|
|
value: remainingTraffic,
|
|
max: totalTraffic,
|
|
displayValue: `${formattedRemaining.value}${formattedRemaining.unit}`,
|
|
unit: '',
|
|
description: `${t('page.headerbanner.monthflowr')} (${formattedTotal.value}${formattedTotal.unit})`,
|
|
subTitle: t('page.headerbanner.Used') + `: ${formattedUsed.value}${formattedUsed.unit}`
|
|
};
|
|
|
|
// 获取当前速率
|
|
const currentActivity = Number(response.activity ?? 0);
|
|
|
|
// 更新峰值速率
|
|
if (currentActivity > peakTrafficRate.value) {
|
|
peakTrafficRate.value = currentActivity;
|
|
}
|
|
|
|
// 格式化当前速度和峰值速度
|
|
const currentSpeed = formatSpeed(currentActivity);
|
|
const peakSpeed = formatSpeed(peakTrafficRate.value);
|
|
|
|
// 设置速率的最大值
|
|
const minMax = 1024; // 1KB/s 作为最小最大值
|
|
const dynamicMax = Math.max(
|
|
currentSpeed.value * 1.5,
|
|
peakSpeed.value,
|
|
minMax
|
|
);
|
|
|
|
// 更新速率数据
|
|
baseData.value[2] = {
|
|
...baseData.value[2],
|
|
value: currentSpeed.value,
|
|
unit: currentSpeed.unit,
|
|
max: dynamicMax,
|
|
subTitle: t('page.headerbanner.maxspeed') + `: ${peakSpeed.value}${peakSpeed.unit}`
|
|
};
|
|
|
|
// 在数据处理完成后,添加自动上网检查
|
|
checkAndTriggerAuth(response);
|
|
|
|
// 统一更新所有图表
|
|
updateGauge1(opts => updateGaugeData(opts, baseData.value[0]));
|
|
updateGauge2(opts => {
|
|
const newOpts = updateGaugeData(opts, baseData.value[1], progressRatio);
|
|
// 添加动画配置
|
|
return {
|
|
...newOpts,
|
|
animation: true,
|
|
animationDuration: 1000,
|
|
animationEasing: 'cubicInOut'
|
|
};
|
|
});
|
|
updateGauge3(opts => updateGaugeData(opts, baseData.value[2]));
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch dashboard data:', error);
|
|
}
|
|
}
|
|
|
|
// 添加 timer 声明
|
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
// 初始化
|
|
async function init() {
|
|
// 立即执行一次数据更新
|
|
await mockDataUpdate();
|
|
// 设置定期执行的定时器
|
|
timer = setInterval(mockDataUpdate, 30000);
|
|
}
|
|
|
|
// 组件卸载时清除定时器
|
|
onUnmounted(() => {
|
|
if (timer) {
|
|
clearInterval(timer);
|
|
timer = null;
|
|
}
|
|
peakTrafficRate.value = 0;
|
|
});
|
|
|
|
// 监听语言变化
|
|
watch(
|
|
() => appStore.locale,
|
|
() => {
|
|
//updateLocale();
|
|
}
|
|
);
|
|
|
|
// 初始化
|
|
init();
|
|
|
|
// 导出更新数据的方法
|
|
const updateDashboard = async () => {
|
|
await mockDataUpdate();
|
|
};
|
|
|
|
// 将方法暴露给父组件
|
|
defineExpose({
|
|
updateDashboard
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<ACard :bordered="false" class="card-wrapper">
|
|
<ARow :gutter="[4, 4]" class="justify-around gauge-row">
|
|
<ACol :span="8" v-for="item in gaugeData" :key="item.id">
|
|
<div class="gauge-container">
|
|
<div class="gauge-chart" :ref="el => {
|
|
if (item.id === 0) gauge1Ref = el as HTMLElement
|
|
else if (item.id === 1) gauge2Ref = el as HTMLElement
|
|
else if (item.id === 2) gauge3Ref = el as HTMLElement
|
|
}"></div>
|
|
<div class="gauge-info">
|
|
<div class="gauge-title">{{ item.title }}</div>
|
|
<div class="gauge-value">{{ item.displayValue || `${item.value}${item.unit}` }}</div>
|
|
<div class="gauge-desc">{{ item.description }}</div>
|
|
<div class="sub-title">{{ item.subTitle }}</div>
|
|
</div>
|
|
</div>
|
|
</ACol>
|
|
</ARow>
|
|
</ACard>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.card-wrapper {
|
|
margin-bottom: 16px;
|
|
background: linear-gradient(180deg, #4284f5 0%, #0c47a7 100%);
|
|
padding: 20px 16px;
|
|
}
|
|
|
|
.gauge-container {
|
|
height: 220px;
|
|
min-height: 140px;
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.gauge-chart {
|
|
flex: 1;
|
|
min-height: 120px;
|
|
position: relative;
|
|
}
|
|
|
|
:deep(.echarts-container) {
|
|
position: absolute !important;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
}
|
|
|
|
.gauge-info {
|
|
padding: 4px 0;
|
|
text-align: center;
|
|
color: #fff;
|
|
margin-top: -20px;
|
|
}
|
|
|
|
.gauge-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.gauge-title {
|
|
font-size: 14px;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.sub-title {
|
|
color: rgba(255, 255, 255, 0.85);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.gauge-desc {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
margin: 2px 0;
|
|
}
|
|
|
|
/* 在小屏幕下调整样式 */
|
|
@media screen and (max-width: 768px) {
|
|
.card-wrapper {
|
|
margin-bottom: 8px;
|
|
padding: 8px 4px;
|
|
}
|
|
|
|
:deep(.arco-row) {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
:deep(.arco-col) {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.gauge-container {
|
|
height: 160px;
|
|
min-height: 90px;
|
|
}
|
|
|
|
.gauge-chart {
|
|
min-height: 70px;
|
|
}
|
|
|
|
.gauge-info {
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.gauge-value {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.gauge-title {
|
|
font-size: 12px;
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.sub-title {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.gauge-desc {
|
|
font-size: 10px;
|
|
}
|
|
|
|
/* 调整仪表盘在小屏幕下的置 */
|
|
:deep(.echarts-container) {
|
|
top: 0;
|
|
transform: scale(0.9);
|
|
transform-origin: center 45%;
|
|
}
|
|
|
|
.gauge-info {
|
|
margin-top: -15px;
|
|
}
|
|
}
|
|
|
|
/* 在超小屏幕下进一步优化 */
|
|
@media screen and (max-width: 375px) {
|
|
.card-wrapper {
|
|
padding: 4px 2px;
|
|
}
|
|
|
|
.gauge-container {
|
|
height: 140px;
|
|
min-height: 80px;
|
|
}
|
|
|
|
.gauge-chart {
|
|
min-height: 60px;
|
|
}
|
|
|
|
:deep(.echarts-container) {
|
|
top: 0;
|
|
transform: scale(0.85);
|
|
transform-origin: center 40%;
|
|
}
|
|
|
|
.gauge-info {
|
|
margin-top: -10px;
|
|
padding: 1px 0;
|
|
}
|
|
}
|
|
</style>
|