mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-03 06:26:51 +08:00
280 lines
7.2 KiB
Vue
280 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { useTheme } from 'vuetify'
|
|
import { hexToRgb } from '@layouts/utils'
|
|
import api from '@/api'
|
|
import { formatDashboardFileSize, useAnimatedDashboardNumber } from '@/composables/useDashboardMotion'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useBackground } from '@/composables/useBackground'
|
|
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
|
|
|
// 国际化
|
|
const { t } = useI18n()
|
|
const { useDataRefresh } = useBackground()
|
|
|
|
// 输入参数
|
|
const props = defineProps({
|
|
// 是否允许刷新数据
|
|
allowRefresh: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
})
|
|
|
|
const vuetifyTheme = useTheme()
|
|
|
|
const currentTheme = controlledComputed(
|
|
() => vuetifyTheme.name.value,
|
|
() => vuetifyTheme.current.value.colors,
|
|
)
|
|
const variableTheme = controlledComputed(
|
|
() => vuetifyTheme.name.value,
|
|
() => vuetifyTheme.current.value.variables,
|
|
)
|
|
|
|
// 时间序列 - 上行和下行流量
|
|
const series = ref([
|
|
{
|
|
name: t('dashboard.upload'),
|
|
data: [0],
|
|
},
|
|
{
|
|
name: t('dashboard.download'),
|
|
data: [0],
|
|
},
|
|
])
|
|
|
|
// 当前值
|
|
const currentUpload = ref(0)
|
|
const currentDownload = ref(0)
|
|
const animatedCurrentUpload = useAnimatedDashboardNumber(currentUpload, {
|
|
duration: 520,
|
|
})
|
|
const animatedCurrentDownload = useAnimatedDashboardNumber(currentDownload, {
|
|
duration: 520,
|
|
})
|
|
const animatedCurrentUploadText = computed(
|
|
() => `${formatDashboardFileSize(animatedCurrentUpload.value, 2, currentUpload.value)}/s`,
|
|
)
|
|
const animatedCurrentDownloadText = computed(
|
|
() => `${formatDashboardFileSize(animatedCurrentDownload.value, 2, currentDownload.value)}/s`,
|
|
)
|
|
|
|
// 根据最近上、下行峰值自动选择图表刻度,低流量时仍保留可读的区域高度。
|
|
const networkChartMax = computed(() => {
|
|
const peak = Math.max(...series.value.flatMap(item => item.data), currentUpload.value, currentDownload.value)
|
|
if (peak <= 0) return 1024
|
|
|
|
const unit = 1024 ** Math.max(0, Math.floor(Math.log(peak) / Math.log(1024)))
|
|
|
|
return Math.max(unit, Math.ceil(peak / unit) * unit)
|
|
})
|
|
|
|
const chartOptions = controlledComputed(
|
|
() => `${vuetifyTheme.name.value}:${networkChartMax.value}`,
|
|
() => {
|
|
const axisLabelColor = `rgba(${hexToRgb(currentTheme.value['on-surface'])},${variableTheme.value['medium-emphasis-opacity']})`
|
|
|
|
return {
|
|
chart: {
|
|
parentHeightOffset: 0,
|
|
toolbar: { show: false },
|
|
zoom: { enabled: false, allowMouseWheelZoom: false },
|
|
selection: { enabled: false },
|
|
animations: { enabled: false },
|
|
foreColor: axisLabelColor,
|
|
},
|
|
tooltip: {
|
|
enabled: false,
|
|
},
|
|
grid: {
|
|
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${
|
|
variableTheme.value['border-opacity']
|
|
})`,
|
|
strokeDashArray: 6,
|
|
xaxis: {
|
|
lines: { show: false },
|
|
},
|
|
yaxis: {
|
|
lines: { show: true },
|
|
},
|
|
padding: {
|
|
top: -10,
|
|
left: 0,
|
|
right: 5,
|
|
bottom: 5,
|
|
},
|
|
},
|
|
stroke: {
|
|
width: 3,
|
|
lineCap: 'butt',
|
|
curve: 'smooth',
|
|
},
|
|
colors: [currentTheme.value.warning, currentTheme.value.info],
|
|
fill: {
|
|
opacity: [0.2, 0.12],
|
|
},
|
|
markers: {
|
|
size: 6,
|
|
offsetY: 4,
|
|
offsetX: 4,
|
|
strokeWidth: 3,
|
|
colors: ['transparent'],
|
|
strokeColors: 'transparent',
|
|
discrete: [
|
|
{
|
|
size: 5.5,
|
|
seriesIndex: 0,
|
|
strokeColor: currentTheme.value.warning,
|
|
fillColor: currentTheme.value.surface,
|
|
},
|
|
{
|
|
size: 5.5,
|
|
seriesIndex: 1,
|
|
strokeColor: currentTheme.value.info,
|
|
fillColor: currentTheme.value.surface,
|
|
},
|
|
],
|
|
hover: { size: 7 },
|
|
},
|
|
dataLabels: {
|
|
enabled: false,
|
|
},
|
|
xaxis: {
|
|
labels: { show: false },
|
|
axisTicks: { show: false },
|
|
axisBorder: { show: false },
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
show: true,
|
|
formatter: (value: number) =>
|
|
`${formatDashboardFileSize(value, value >= 1024 ** 2 ? 1 : 0, networkChartMax.value)}/s`,
|
|
style: {
|
|
colors: axisLabelColor,
|
|
fontSize: '10px',
|
|
},
|
|
},
|
|
tickAmount: 2,
|
|
max: networkChartMax.value,
|
|
min: 0,
|
|
},
|
|
legend: {
|
|
show: false,
|
|
},
|
|
}
|
|
},
|
|
)
|
|
|
|
// 调用API接口获取最新网络流量
|
|
async function getNetworkUsage() {
|
|
if (!props.allowRefresh) return
|
|
try {
|
|
// 请求数据 - 接口返回 [上行流量, 下行流量]
|
|
const data: [number, number] = (await api.get('dashboard/network', {
|
|
feedback: 'silent',
|
|
skipNavigationCancellation: true,
|
|
})) ?? [0, 0]
|
|
currentUpload.value = Number(data[0]) || 0
|
|
currentDownload.value = Number(data[1]) || 0
|
|
|
|
// 使用nextTick确保DOM更新完成后再更新图表数据
|
|
await nextTick()
|
|
|
|
// 添加到序列
|
|
series.value[0].data.push(currentUpload.value)
|
|
series.value[1].data.push(currentDownload.value)
|
|
|
|
// 序列超过30条记录时,清掉前面的
|
|
if (series.value[0].data.length > 30) {
|
|
series.value[0].data.shift()
|
|
series.value[1].data.shift()
|
|
}
|
|
} catch (e) {
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
// 使用数据刷新定时器
|
|
const { refresh } = useDataRefresh(
|
|
'dashboard-network',
|
|
getNetworkUsage,
|
|
2000, // 2秒间隔
|
|
true, // 立即执行
|
|
)
|
|
|
|
useKeepAliveRefresh(refresh)
|
|
</script>
|
|
|
|
<template>
|
|
<VCard class="dashboard-chart-card dashboard-grid-fill">
|
|
<VCardItem>
|
|
<template #prepend><VIcon icon="mdi-swap-vertical-bold" size="20" class="me-2" /></template>
|
|
<VCardTitle>{{ t('dashboard.network') }}</VCardTitle>
|
|
</VCardItem>
|
|
<VCardText class="dashboard-chart-content">
|
|
<div class="dashboard-chart-plot">
|
|
<VApexChart type="area" :options="chartOptions" :series="series" height="100%" />
|
|
</div>
|
|
<div class="dashboard-chart-footer">
|
|
<span
|
|
><i class="network-dot network-dot--upload" />{{ t('dashboard.upload') }}
|
|
{{ animatedCurrentUploadText }}</span
|
|
>
|
|
<span
|
|
><i class="network-dot network-dot--download" />{{ t('dashboard.download') }}
|
|
{{ animatedCurrentDownloadText }}</span
|
|
>
|
|
</div>
|
|
</VCardText>
|
|
</VCard>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.dashboard-chart-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
block-size: 100%;
|
|
min-block-size: 270px;
|
|
}
|
|
|
|
.dashboard-chart-content {
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
flex-direction: column;
|
|
min-block-size: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dashboard-chart-plot {
|
|
flex: 1 1 auto;
|
|
min-block-size: 120px;
|
|
}
|
|
|
|
.dashboard-chart-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
font-size: 0.68rem;
|
|
gap: 0.6rem;
|
|
font-variant-numeric: tabular-nums;
|
|
padding-block-start: 0.55rem;
|
|
}
|
|
|
|
.network-dot {
|
|
display: inline-block;
|
|
border-radius: 50%;
|
|
block-size: 6px;
|
|
inline-size: 6px;
|
|
margin-inline-end: 0.3rem;
|
|
}
|
|
|
|
.network-dot--upload {
|
|
background: rgb(var(--v-theme-warning));
|
|
}
|
|
|
|
.network-dot--download {
|
|
background: rgb(var(--v-theme-info));
|
|
}
|
|
</style>
|