mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
在仪表板中添加网络流量组件
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.6.0",
|
"version": "2.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
|||||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||||
|
import AnalyticsNetwork from '@/views/dashboard/AnalyticsNetwork.vue'
|
||||||
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||||
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||||
@@ -81,6 +82,7 @@ onUnmounted(() => {
|
|||||||
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" :allowRefresh="props.allowRefresh" />
|
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" :allowRefresh="props.allowRefresh" />
|
||||||
<AnalyticsCpu v-else-if="config?.id === 'cpu'" :allowRefresh="props.allowRefresh" />
|
<AnalyticsCpu v-else-if="config?.id === 'cpu'" :allowRefresh="props.allowRefresh" />
|
||||||
<AnalyticsMemory v-else-if="config?.id === 'memory'" :allowRefresh="props.allowRefresh" />
|
<AnalyticsMemory v-else-if="config?.id === 'memory'" :allowRefresh="props.allowRefresh" />
|
||||||
|
<AnalyticsNetwork v-else-if="config?.id === 'network'" :allowRefresh="props.allowRefresh" />
|
||||||
<MediaServerLibrary v-else-if="config?.id === 'library'" />
|
<MediaServerLibrary v-else-if="config?.id === 'library'" />
|
||||||
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
|
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
|
||||||
<MediaServerLatest v-else-if="config?.id === 'latest'" />
|
<MediaServerLatest v-else-if="config?.id === 'latest'" />
|
||||||
|
|||||||
@@ -580,6 +580,9 @@ export default {
|
|||||||
scheduler: 'Background Tasks',
|
scheduler: 'Background Tasks',
|
||||||
cpu: 'CPU',
|
cpu: 'CPU',
|
||||||
memory: 'Memory',
|
memory: 'Memory',
|
||||||
|
network: 'Network Traffic',
|
||||||
|
upload: 'Upload',
|
||||||
|
download: 'Download',
|
||||||
library: 'My Media Library',
|
library: 'My Media Library',
|
||||||
playing: 'Continue Watching',
|
playing: 'Continue Watching',
|
||||||
latest: 'Recently Added',
|
latest: 'Recently Added',
|
||||||
|
|||||||
@@ -578,6 +578,9 @@ export default {
|
|||||||
scheduler: '后台任务',
|
scheduler: '后台任务',
|
||||||
cpu: 'CPU',
|
cpu: 'CPU',
|
||||||
memory: '内存',
|
memory: '内存',
|
||||||
|
network: '网络流量',
|
||||||
|
upload: '上行',
|
||||||
|
download: '下行',
|
||||||
library: '我的媒体库',
|
library: '我的媒体库',
|
||||||
playing: '继续观看',
|
playing: '继续观看',
|
||||||
latest: '最近添加',
|
latest: '最近添加',
|
||||||
|
|||||||
@@ -576,6 +576,9 @@ export default {
|
|||||||
scheduler: '後台任務',
|
scheduler: '後台任務',
|
||||||
cpu: 'CPU',
|
cpu: 'CPU',
|
||||||
memory: '內存',
|
memory: '內存',
|
||||||
|
network: '網絡流量',
|
||||||
|
upload: '上行',
|
||||||
|
download: '下行',
|
||||||
library: '我的媒體庫',
|
library: '我的媒體庫',
|
||||||
playing: '繼續觀看',
|
playing: '繼續觀看',
|
||||||
latest: '最近添加',
|
latest: '最近添加',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const enableConfig = ref<{ [key: string]: boolean }>({
|
|||||||
weeklyOverview: false,
|
weeklyOverview: false,
|
||||||
cpu: false,
|
cpu: false,
|
||||||
memory: false,
|
memory: false,
|
||||||
|
network: false,
|
||||||
library: true,
|
library: true,
|
||||||
playing: true,
|
playing: true,
|
||||||
latest: true,
|
latest: true,
|
||||||
@@ -112,6 +113,14 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
|||||||
cols: { cols: 12, md: 6 },
|
cols: { cols: 12, md: 6 },
|
||||||
elements: [],
|
elements: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'network',
|
||||||
|
name: t('dashboard.network'),
|
||||||
|
key: '',
|
||||||
|
attrs: {},
|
||||||
|
cols: { cols: 12, md: 6 },
|
||||||
|
elements: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'library',
|
id: 'library',
|
||||||
name: t('dashboard.library'),
|
name: t('dashboard.library'),
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTheme } from 'vuetify'
|
||||||
|
import { hexToRgb } from '@layouts/utils'
|
||||||
|
import api from '@/api'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
// 国际化
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
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 chartKey = ref(0)
|
||||||
|
|
||||||
|
// 定时器
|
||||||
|
let refreshTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
// 时间序列 - 上行和下行流量
|
||||||
|
const series = ref([
|
||||||
|
{
|
||||||
|
name: '上行流量',
|
||||||
|
data: [0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '下行流量',
|
||||||
|
data: [0],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 当前值
|
||||||
|
const currentUpload = ref(0)
|
||||||
|
const currentDownload = ref(0)
|
||||||
|
|
||||||
|
// 格式化流量显示
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B/s'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartOptions = controlledComputed(
|
||||||
|
() => vuetifyTheme.name.value,
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
chart: {
|
||||||
|
parentHeightOffset: 0,
|
||||||
|
toolbar: { show: false },
|
||||||
|
animations: { enabled: false },
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
y: {
|
||||||
|
formatter: (value: number) => formatBytes(value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: -7,
|
||||||
|
right: 5,
|
||||||
|
bottom: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: 3,
|
||||||
|
lineCap: 'butt',
|
||||||
|
curve: 'smooth',
|
||||||
|
},
|
||||||
|
colors: [currentTheme.value.warning, currentTheme.value.info],
|
||||||
|
markers: {
|
||||||
|
size: 6,
|
||||||
|
offsetY: 4,
|
||||||
|
offsetX: -2,
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
labels: { show: false },
|
||||||
|
axisTicks: { show: false },
|
||||||
|
axisBorder: { show: false },
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: { show: false },
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
horizontalAlign: 'left',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 调用API接口获取最新网络流量
|
||||||
|
async function getNetworkUsage() {
|
||||||
|
if (!props.allowRefresh) return
|
||||||
|
try {
|
||||||
|
// 请求数据 - 接口返回 [上行流量, 下行流量]
|
||||||
|
const data: [number, number] = (await api.get('dashboard/network')) ?? [0, 0]
|
||||||
|
currentUpload.value = data[0] || 0
|
||||||
|
currentDownload.value = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 延迟启动,确保组件完全挂载
|
||||||
|
nextTick(() => {
|
||||||
|
getNetworkUsage()
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
getNetworkUsage()
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时停止定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
refreshTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
chartKey.value += 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard v-bind="hover.props">
|
||||||
|
<VCardItem>
|
||||||
|
<template #append>
|
||||||
|
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>{{ t('dashboard.network') }}</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VCardText>
|
||||||
|
<VApexChart :key="chartKey" type="line" :options="chartOptions" :series="series" :height="150" />
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<p class="text-center font-weight-medium mb-0">
|
||||||
|
<span class="text-primary">{{ t('dashboard.upload') }}</span
|
||||||
|
>:{{ formatBytes(currentUpload) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-center font-weight-medium mb-0">
|
||||||
|
<span class="text-success">{{ t('dashboard.download') }}</span
|
||||||
|
>:{{ formatBytes(currentDownload) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user