fix:首页头部仪表盘
This commit is contained in:
@@ -1,62 +1,462 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, watch, ref } from 'vue';
|
||||||
import { $t } from '@/locales';
|
import { useEcharts } from '@/hooks/common/echarts';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
|
import type { ECOption } from '@/hooks/common/echarts';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'HeaderBanner'
|
name: 'HeaderBanner'
|
||||||
});
|
});
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
interface StatisticData {
|
interface GaugeData {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value: number;
|
||||||
|
max: number;
|
||||||
|
unit: string;
|
||||||
|
subTitle?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statisticData = computed<StatisticData[]>(() => [
|
// 使用 ref 来存储基础数据
|
||||||
|
const baseData = ref([
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
title: $t('page.home.projectCount'),
|
title: '剩余话费',
|
||||||
value: '25'
|
value: 23,
|
||||||
|
max: 100,
|
||||||
|
unit: '元',
|
||||||
|
description: '本月话费',
|
||||||
|
subTitle: '已用: 77元'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: $t('page.home.todo'),
|
title: '剩余流量',
|
||||||
value: '4/16'
|
value: 890,
|
||||||
|
max: 1024,
|
||||||
|
unit: 'MB',
|
||||||
|
description: '本月流量',
|
||||||
|
subTitle: '已用: 134MB'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: $t('page.home.message'),
|
title: '流量速率',
|
||||||
value: '12'
|
value: 3,
|
||||||
|
max: 10,
|
||||||
|
unit: 'MB/s',
|
||||||
|
description: '当前速度',
|
||||||
|
subTitle: '峰值: 8MB/s'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 计算属性保持不变
|
||||||
|
const gaugeData = computed(() => baseData.value);
|
||||||
|
|
||||||
|
// 仪盘配置生成函数
|
||||||
|
const getGaugeOptions = (data: GaugeData): 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: GaugeData) => {
|
||||||
|
if (!opts.series || !Array.isArray(opts.series)) {
|
||||||
|
opts.series = [{
|
||||||
|
name: data.title,
|
||||||
|
type: 'gauge',
|
||||||
|
min: 0,
|
||||||
|
max: data.max,
|
||||||
|
data: [],
|
||||||
|
detail: { show: false },
|
||||||
|
title: { show: false }
|
||||||
|
}] as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一个系列并使用类型断言
|
||||||
|
const series = opts.series[0] as {
|
||||||
|
axisLine?: {
|
||||||
|
lineStyle: {
|
||||||
|
width: number;
|
||||||
|
color: Array<[number, string]>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
data: Array<{
|
||||||
|
value: number;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新轴线颜色
|
||||||
|
if (!series.axisLine) {
|
||||||
|
series.axisLine = {
|
||||||
|
lineStyle: {
|
||||||
|
width: 15,
|
||||||
|
color: [
|
||||||
|
[(data.value / data.max), '#4284f5'],
|
||||||
|
[1, '#0c47a7']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
series.axisLine.lineStyle = {
|
||||||
|
width: 15,
|
||||||
|
color: [
|
||||||
|
[(data.value / data.max), '#4284f5'],
|
||||||
|
[1, '#0c47a7']
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
series.data = [{
|
||||||
|
value: data.value,
|
||||||
|
name: data.title
|
||||||
|
}];
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新话费和流量数据的函数
|
||||||
|
function updateBillingData() {
|
||||||
|
// 更新话费数据
|
||||||
|
const remainingBill = Number((Math.random() * 50).toFixed(2)); // 0-50元
|
||||||
|
const usedBill = Number((100 - remainingBill).toFixed(2));
|
||||||
|
baseData.value[0] = {
|
||||||
|
...baseData.value[0],
|
||||||
|
value: remainingBill,
|
||||||
|
subTitle: `已用: ${usedBill}元`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新流量数据
|
||||||
|
const remainingData = Number((Math.random() * 1024).toFixed(2)); // 0-1024MB
|
||||||
|
const usedData = Number((1024 - remainingData).toFixed(2));
|
||||||
|
baseData.value[1] = {
|
||||||
|
...baseData.value[1],
|
||||||
|
value: remainingData,
|
||||||
|
subTitle: `已用: ${usedData}MB`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
updateGauge1(opts => updateGaugeData(opts, baseData.value[0]));
|
||||||
|
updateGauge2(opts => updateGaugeData(opts, baseData.value[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
async function mockData() {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateGauge1(opts => updateGaugeData(opts, gaugeData.value[0]));
|
||||||
|
updateGauge2(opts => updateGaugeData(opts, gaugeData.value[1]));
|
||||||
|
updateGauge3(opts => updateGaugeData(opts, gaugeData.value[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新语言
|
||||||
|
function updateLocale() {
|
||||||
|
updateGauge1((opts) => updateGaugeData(opts, gaugeData.value[0]));
|
||||||
|
updateGauge2((opts) => updateGaugeData(opts, gaugeData.value[1]));
|
||||||
|
updateGauge3((opts) => updateGaugeData(opts, gaugeData.value[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改初始峰值为 0
|
||||||
|
const peakSpeed = ref(0); // 初始峰值设为 0MB/s
|
||||||
|
|
||||||
|
// 重置峰值函数
|
||||||
|
function resetPeakSpeed() {
|
||||||
|
peakSpeed.value = 0; // 重置为 0
|
||||||
|
// 同时更新显示
|
||||||
|
baseData.value[2] = {
|
||||||
|
...baseData.value[2],
|
||||||
|
value: 0, // 同时也将当前值重置为 0
|
||||||
|
subTitle: `峰值: ${peakSpeed.value.toFixed(2)}MB/s`
|
||||||
|
};
|
||||||
|
// 更新图表
|
||||||
|
updateGauge3(opts => updateGaugeData(opts, baseData.value[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟数据更新
|
||||||
|
function mockDataUpdate() {
|
||||||
|
// 先重置峰值
|
||||||
|
resetPeakSpeed();
|
||||||
|
|
||||||
|
// 更新话费和流量数据
|
||||||
|
updateBillingData();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// 更新话费和流量数据(每30秒更新一次)
|
||||||
|
if (Math.random() > 0.5) { // 50%的概率更新
|
||||||
|
updateBillingData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟流量速率变化
|
||||||
|
const speedValue = Math.random() * 6 + 1; // 1-7 MB/s
|
||||||
|
|
||||||
|
// 更新峰值
|
||||||
|
peakSpeed.value = Math.max(peakSpeed.value, speedValue);
|
||||||
|
|
||||||
|
// 更新基础数据
|
||||||
|
baseData.value[2] = {
|
||||||
|
...baseData.value[2],
|
||||||
|
value: Number(speedValue.toFixed(2)),
|
||||||
|
subTitle: `峰值: ${peakSpeed.value.toFixed(2)}MB/s`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
updateGauge3(opts => updateGaugeData(opts, baseData.value[2]));
|
||||||
|
}, 3000); // 每3秒更新一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
async function init() {
|
||||||
|
await mockData();
|
||||||
|
mockDataUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听语言变化
|
||||||
|
watch(
|
||||||
|
() => appStore.locale,
|
||||||
|
() => {
|
||||||
|
updateLocale();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ACard :bordered="false" class="card-wrapper">
|
<ACard :bordered="false" class="card-wrapper">
|
||||||
<ARow :gutter="[16, 16]">
|
<ARow :gutter="[4, 4]" class="justify-around gauge-row">
|
||||||
<ACol :span="24" :md="18">
|
<ACol :span="8" v-for="item in gaugeData" :key="item.id">
|
||||||
<div class="flex-y-center">
|
<div class="gauge-container">
|
||||||
<div class="size-72px shrink-0 overflow-hidden rd-1/2">
|
<div class="gauge-chart" :ref="el => {
|
||||||
<img src="@/assets/imgs/soybean.jpg" class="size-full" />
|
if (item.id === 0) gauge1Ref = el as HTMLElement
|
||||||
</div>
|
else if (item.id === 1) gauge2Ref = el as HTMLElement
|
||||||
<div class="pl-12px">
|
else if (item.id === 2) gauge3Ref = el as HTMLElement
|
||||||
<h3 class="text-18px font-semibold">
|
}"></div>
|
||||||
{{ $t('page.home.greeting', { username: authStore.userInfo.username }) }}
|
<div class="gauge-info">
|
||||||
</h3>
|
<div class="gauge-title">{{ item.title }}</div>
|
||||||
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
|
<div class="gauge-value">{{ item.value }}{{ item.unit }}</div>
|
||||||
|
<div class="gauge-desc">{{ item.description }}</div>
|
||||||
|
<div class="sub-title">{{ item.subTitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ACol>
|
</ACol>
|
||||||
<ACol :span="24" :md="6">
|
|
||||||
<ASpace class="w-full justify-end" :size="24">
|
|
||||||
<AStatistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item" />
|
|
||||||
</ASpace>
|
|
||||||
</ACol>
|
|
||||||
</ARow>
|
</ARow>
|
||||||
</ACard>
|
</ACard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user