2
0
Files
fe.wfc.user/src/views/home/modules/header-banner.vue
2025-02-26 18:33:43 +08:00

797 lines
19 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 { 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;
speedLimits?: {
upLimit: string;
downLimit: 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' };
}
};
// 添加格式化余额的函数
const formatBalance = (balance: number | string): string => {
// 确保转换为数字类型
const numBalance = Number(balance);
// 检查是否是有效数字
if (isNaN(numBalance)) {
return '0.00';
}
return numBalance.toFixed(2);
};
// 修改基础数据,移除速率仪表盘
const baseData = ref<GaugeDisplayData[]>([
{
id: 0,
title: t('page.headerbanner.Remainingcredit'),
value: 0,
max: 100,
unit: t('page.headerbanner.money'),
description: t('page.headerbanner.monthphonebill'),
subTitle: t('page.headerbanner.deviceCount') + `: 0${t('page.headerbanner.device')}`,
displayValue: '-'
},
{
id: 1,
title: t('page.headerbanner.Flowremaining'),
value: 0,
max: 1024,
unit: 'B',
//description: t('page.headerbanner.monthflowr'),
//subTitle: t('page.headerbanner.Used') + ': 0B',
description: '-',
subTitle: '-',
speedLimits: {
// upLimit: t('page.headerbanner.nolimit'),
// downLimit: t('page.headerbanner.nolimit')
upLimit: '-',
downLimit: '-'
}
}
]);
// 计算属性保持不变
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 || 1), '#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 updateGaugeData = (opts: ECOption, data: GaugeDisplayData, progressRatio?: number) => {
const ratio = progressRatio !== undefined ? progressRatio : (data.value / (data.max || 1));
// 创建完整的新配置
const newOpts: ECOption = {
backgroundColor: 'transparent',
series: [{
name: data.title,
type: 'gauge',
min: 0,
max: data.max || 1,
startAngle: 200,
endAngle: -20,
radius: '85%',
center: ['50%', '55%'],
axisLine: {
lineStyle: {
width: 15,
color: [
[ratio, '#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;
};
// 修改速率格式化函数
const formatSpeedLimit = (speed: number | null): string => {
if (speed === null || speed === undefined) {
return '0Kbps';
}
if (speed === -1) {
return t('page.headerbanner.nolimit');
}
if (speed >= 1048576) {
return `${(speed / 1048576).toFixed(1)}Gbps`;
} else if (speed >= 1024) {
return `${(speed / 1024).toFixed(1)}Mbps`;
}
return `${speed}Kbps`;
};
// 添加速率限制的响应式引用
const speedLimits = ref({
// upLimit: t('page.headerbanner.nolimit'),
// downLimit: t('page.headerbanner.nolimit')
upLimit: '-',
downLimit: '-'
});
// 添加套餐信息的响应式引用
const packageInfo = ref({
// packageName: '',
// price: '0.00'
packageName: t('page.headerbanner.noPackage'),
price: '-'
});
// 修改数据更新函数,添加套餐信息的更新
async function mockDataUpdate() {
try {
const response = await authStore.getDashboardData();
if (response && typeof response === 'object' && !response.error) {
// 更新套餐信息
packageInfo.value = {
packageName: response.packageName || t('page.headerbanner.nopackage'),
price: response.packageName ? formatBalance(response.price) : '-' // 有套餐时才格式化价格
};
// 更新余额和设备数据
const numBalance = Number(response.balance);
baseData.value[0] = {
...baseData.value[0],
value: numBalance,
max: Math.max(numBalance, 100),
displayValue:formatBalance(response.balance),
unit: t('page.headerbanner.money'),
subTitle: t('page.headerbanner.deviceCount') + `: ${
response.packageName ? (
!response.clientNumEnable
? t('page.headerbanner.nolimit')
: (response.clientNum || 0) + t('page.headerbanner.device')
):'-'
}`
};
// 更新流量数据
if (!response.packageName) {
// 无套餐时显示 -
baseData.value[1] = {
...baseData.value[1],
value: 0,
max: 1024,
displayValue: '-',
description: '-',
subTitle: '-',
speedLimits: {
upLimit: '-',
downLimit: '-'
}
};
} else {
// 有套餐时的正常显示逻辑
const totalTraffic = response.trafficEnable ? (response.traffic || 0) : 0;
const usedTraffic = response.trafficEnable ? (response.trafficUsed || 0) : 0;
const remainingTraffic = Math.max(0, totalTraffic - usedTraffic);
// 格式化流量显示
const formattedTotal = formatTraffic(totalTraffic);
const formattedUsed = formatTraffic(usedTraffic);
const formattedRemaining = formatTraffic(remainingTraffic);
// 更新流量数据显示
// baseData.value[1] = {
// ...baseData.value[1],
// value: remainingTraffic,
// max: totalTraffic,
// displayValue: !response.trafficEnable ? t('page.headerbanner.nolimit') : `${formattedRemaining.value}${formattedRemaining.unit}`,
// unit: '',
// description: !response.trafficEnable
// ? t('page.headerbanner.nolimit')
// : `${formattedTotal.value}${formattedTotal.unit}`,
// subTitle: !response.trafficEnable
// ? t('page.headerbanner.nolimit')
// : formattedUsed.value+formattedUsed.unit,
// speedLimits: {
// upLimit: speedLimits.value.upLimit,
// downLimit: speedLimits.value.downLimit
// }
// };
// console.log(baseData.value[1].description)
baseData.value[1] = {
...baseData.value[1],
value: remainingTraffic,
max: totalTraffic,
displayValue: response.trafficEnable ? `${formattedRemaining.value}${formattedRemaining.unit}` : t('page.headerbanner.nolimit'),
description: response.trafficEnable ? `${formattedTotal.value}${formattedTotal.unit}` : t('page.headerbanner.nolimit'),
subTitle: response.trafficEnable ? formattedUsed.value+formattedUsed.unit : t('page.headerbanner.nolimit')
};
}
// 更新速率限制显示
if (!response.packageName) {
speedLimits.value = {
upLimit: '-',
downLimit: '-'
};
} else if (response.rateLimitEnable) {
speedLimits.value = {
upLimit: !response.upLimitEnable
? t('page.headerbanner.nolimit')
: formatSpeedLimit(response.upLimit),
downLimit: !response.downLimitEnable
? t('page.headerbanner.nolimit')
: formatSpeedLimit(response.downLimit)
};
} else {
speedLimits.value = {
upLimit: t('page.headerbanner.nolimit'),
downLimit: t('page.headerbanner.nolimit')
};
}
// 更新图表
updateGauge1(opts => updateGaugeData(opts, baseData.value[0]));
updateGauge2(opts => {
const newOpts = updateGaugeData(
opts,
baseData.value[1],
response.packageName && response.trafficEnable ? remainingTraffic / totalTraffic : 0
);
return {
...newOpts,
animation: true,
animationDuration: 1000,
animationEasing: 'cubicInOut'
};
});
}
} catch (error) {
// console.error('Failed to update 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;
}
});
// 监听语言变化
watch(
() => appStore.locale,
() => {
//updateLocale();
}
);
// 初始化
init();
// 导出更新数据的方法
const updateDashboard = async () => {
await mockDataUpdate();
};
// 将方法暴露给父组件
defineExpose({
updateDashboard
});
// 修改流量获取函数
const getTrafficTotal = (description?: string, trafficEnable?: boolean): string => {
// 如果描述中包含"无限制",说明是无限制套餐
if (!description || description.includes(t('page.headerbanner.nolimit'))) {
return t('page.headerbanner.nolimit');
}
const matches = description.match(/\((.*?)\)/);
return matches ? matches[1] : '0B';
};
// 修改设备数量获取函数
const getDeviceCount = (subTitle?: string, clientNumEnable?: boolean): string => {
if (!subTitle) return t('page.headerbanner.nolimit');
const parts = subTitle.split(': ');
return parts.length > 1 ? parts[1] : '0';
};
</script>
<template>
<ACard :bordered="false" class="card-wrapper">
<div class="dashboard-layout">
<!-- 左侧仪表盘和流量信息 -->
<div class="left-section">
<div class="gauge-container">
<div class="gauge-chart" ref="gauge2Ref"></div>
<div class="gauge-info">
<div class="gauge-title">{{ baseData[1].title }}</div>
<div class="gauge-value">{{ baseData[1].displayValue }}</div>
</div>
</div>
</div>
<!-- 右侧分组信息显示 -->
<div class="right-section">
<!-- 套餐信息 -->
<div class="info-section">
<div class="section-title">{{t('page.headerbanner.packageinfo')}}</div>
<div class="info-group">
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.packagename') }}</span>
<span class="info-value">{{ packageInfo.packageName || t('page.headerbanner.nopackage') }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.price') }}</span>
<span class="info-value">{{ packageInfo.price === '-' ? '-' : '¥' + packageInfo.price }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.monthflowr') }}</span>
<span class="info-value">{{ baseData[1].description }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.Used') }}</span>
<span class="info-value">{{ baseData[1].subTitle }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.uplimit') }}</span>
<span class="info-value">{{ speedLimits.upLimit }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.downlimit') }}</span>
<span class="info-value">{{ speedLimits.downLimit }}</span>
</div>
</div>
</div>
<!-- 账户信息 -->
<div class="info-section">
<div class="section-title">{{ t('page.headerbanner.accountinfor') }}</div>
<div class="info-group">
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.Remainingcredit') }}</span>
<span class="info-value">{{ baseData[0].displayValue === '-' ? '-' : '¥' + baseData[0].displayValue }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.client') }}</span>
<span class="info-value">{{ getDeviceCount(baseData[0].subTitle, baseData[0].speedLimits?.upLimit !== t('page.headerbanner.nolimit')) }}</span>
</div>
</div>
</div>
</div>
</div>
</ACard>
</template>
<style scoped>
.card-wrapper {
margin-bottom: 16px;
background: linear-gradient(180deg, #4284f5 0%, #0c47a7 100%);
padding: 16px 12px 16px 8px;
color: #fff;
}
.dashboard-layout {
display: flex;
gap: 4px;
}
/* 左侧部分样式 */
.left-section {
flex: 0 0 auto;
width: 160px;
display: flex;
align-items: center;
justify-content: flex-start;
}
.gauge-container {
text-align: center;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
margin-left: -15px;
}
.gauge-chart {
width: 160px;
height: 160px;
}
.gauge-info {
margin-top: -12px;
}
.gauge-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.85);
}
.gauge-value {
font-size: 16px;
font-weight: bold;
margin-top: 2px;
}
/* 右侧部分样式 */
.right-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding-right: 4px;
}
.info-section {
margin-bottom: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.section-title {
font-size: 14px;
font-weight: 500;
color: #fff;
margin-bottom: 8px;
padding-left: 6px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.section-title::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background-color: #fff;
margin-right: 8px;
border-radius: 2px;
}
.info-group {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 6px 8px;
width: 80%;
margin: 0 auto;
}
.info-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 8px 0;
max-width: 300px;
margin: 0 auto;
}
.info-item:not(:last-child) {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.info-label {
color: rgba(255, 255, 255, 0.85);
font-size: 12px;
}
.info-value {
font-size: 14px;
font-weight: 500;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.card-wrapper {
padding: 16px 8px 16px 6px;
}
.dashboard-layout {
gap: 4px;
}
.right-section {
gap: 4px;
padding-right: 2px;
}
.section-title {
font-size: 14px;
margin-bottom: 6px;
}
.info-section {
margin-bottom: 8px;
}
.info-group {
width: 90%;
}
.info-item {
padding: 6px 0;
max-width: 250px;
}
.info-label {
font-size: 12px;
margin-bottom: 3px;
}
.info-value {
font-size: 14px;
}
.left-section {
width: 120px;
min-height: 180px;
}
.gauge-container {
width: 120px;
margin-left: -10px;
}
.gauge-chart {
width: 120px;
height: 120px;
}
.gauge-info {
margin-top: -12px;
}
.gauge-value {
font-size: 16px;
margin-top: 2px;
}
}
/* 超小屏幕优化 */
@media screen and (max-width: 375px) {
.card-wrapper {
padding: 10px 6px 10px 4px;
}
.dashboard-layout {
gap: 4px;
}
.left-section {
width: 100px;
min-height: 160px;
}
.gauge-container {
width: 100px;
margin-left: -8px;
}
.gauge-chart {
width: 100px;
height: 100px;
}
.gauge-info {
margin-top: -12px;
}
.gauge-value {
font-size: 16px;
}
.section-title {
font-size: 13px;
margin-bottom: 4px;
}
.info-group {
width: 95%;
}
.info-item {
padding: 4px 0;
max-width: 200px;
}
.info-label {
font-size: 11px;
margin-bottom: 2px;
}
.info-value {
font-size: 13px;
}
.right-section {
padding-right: 1px;
}
.info-section {
margin-bottom: 6px;
}
}
</style>