mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 09:13:49 +08:00
820 lines
26 KiB
Vue
820 lines
26 KiB
Vue
<script lang="ts" setup>
|
||
defineOptions({
|
||
name: 'dashboard-index'
|
||
})
|
||
|
||
import { LineChart } from 'echarts/charts'
|
||
import {
|
||
DataZoomComponent,
|
||
GridComponent,
|
||
LegendComponent,
|
||
TitleComponent,
|
||
TooltipComponent
|
||
} from 'echarts/components'
|
||
import { use } from 'echarts/core'
|
||
import { CanvasRenderer } from 'echarts/renderers'
|
||
import { NButton, NPopconfirm } from 'naive-ui'
|
||
import { useI18n } from 'vue-i18n'
|
||
|
||
import dashboard from '@/api/panel/dashboard'
|
||
import { router } from '@/router'
|
||
import { useTabStore } from '@/store'
|
||
import { formatDateTime, formatDuration, toTimestamp } from '@/utils/common'
|
||
import { formatBytes, formatPercent } from '@/utils/file'
|
||
import VChart from 'vue-echarts'
|
||
import type { CountInfo, HomeApp, Realtime, SystemInfo } from './types'
|
||
|
||
use([
|
||
CanvasRenderer,
|
||
LineChart,
|
||
TitleComponent,
|
||
TooltipComponent,
|
||
LegendComponent,
|
||
GridComponent,
|
||
DataZoomComponent
|
||
])
|
||
|
||
const { locale } = useI18n()
|
||
const tabStore = useTabStore()
|
||
const realtime = ref<Realtime | null>(null)
|
||
const systemInfo = ref<SystemInfo | null>(null)
|
||
const homeApps = ref<HomeApp[] | null>(null)
|
||
const homeAppsLoading = ref(true)
|
||
const countInfo = ref<CountInfo>({
|
||
website: 0,
|
||
database: 0,
|
||
ftp: 0,
|
||
cron: 0
|
||
})
|
||
|
||
const nets = ref<Array<string>>([]) // 选择的网卡
|
||
const disks = ref<Array<string>>([]) // 选择的硬盘
|
||
const chartType = ref('net')
|
||
const unitType = ref('KB')
|
||
const units = [
|
||
{ label: 'B', value: 'B' },
|
||
{ label: 'KB', value: 'KB' },
|
||
{ label: 'MB', value: 'MB' },
|
||
{ label: 'GB', value: 'GB' }
|
||
]
|
||
|
||
const cores = ref(0)
|
||
const diskReadBytes = ref<Array<number>>([])
|
||
const diskWriteBytes = ref<Array<number>>([])
|
||
const netBytesSent = ref<Array<number>>([])
|
||
const netBytesRecv = ref<Array<number>>([])
|
||
const timeDiskData = ref<Array<string>>([])
|
||
const timeNetData = ref<Array<string>>([])
|
||
const total = reactive({
|
||
diskReadBytes: 0,
|
||
diskWriteBytes: 0,
|
||
diskRWBytes: 0,
|
||
diskRWTime: 0,
|
||
netBytesSent: 0,
|
||
netBytesRecv: 0
|
||
})
|
||
|
||
const current = reactive({
|
||
diskReadBytes: 0,
|
||
diskWriteBytes: 0,
|
||
diskRWBytes: 0,
|
||
diskRWTime: 0,
|
||
netBytesSent: 0,
|
||
netBytesRecv: 0,
|
||
time: 0
|
||
})
|
||
|
||
const statusColor = (percentage: number) => {
|
||
if (percentage >= 90) {
|
||
return 'var(--error-color)'
|
||
} else if (percentage >= 80) {
|
||
return 'var(--warning-color)'
|
||
} else if (percentage >= 70) {
|
||
return 'var(--info-color)'
|
||
}
|
||
return 'var(--success-color)'
|
||
}
|
||
|
||
const statusText = (percentage: number) => {
|
||
if (percentage >= 90) {
|
||
return '运行堵塞'
|
||
} else if (percentage >= 80) {
|
||
return '运行缓慢'
|
||
} else if (percentage >= 70) {
|
||
return '运行正常'
|
||
}
|
||
return '运行流畅'
|
||
}
|
||
|
||
const chartOptions = computed(() => {
|
||
return {
|
||
title: {
|
||
text: chartType.value == 'net' ? '网络' : '硬盘',
|
||
textAlign: 'left',
|
||
textStyle: {
|
||
fontSize: 20
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'cross'
|
||
},
|
||
formatter: function (params: any) {
|
||
let res = params[0].name + '<br/>'
|
||
params.forEach(function (item: any) {
|
||
res += `${item.marker} ${item.seriesName}: ${item.value} ${unitType.value}<br/>`
|
||
})
|
||
return res
|
||
}
|
||
},
|
||
legend: {
|
||
align: 'left',
|
||
data: chartType.value == 'net' ? ['发送', '接收'] : ['读取', '写入']
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: timeDiskData.value
|
||
},
|
||
yAxis: {
|
||
name: `单位 ${unitType.value}`,
|
||
type: 'value',
|
||
axisLabel: {
|
||
formatter: `{value} ${unitType.value}`
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: chartType.value == 'net' ? '发送' : '读取',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: chartType.value == 'net' ? netBytesSent.value : diskReadBytes.value,
|
||
markPoint: {
|
||
data: [
|
||
{ type: 'max', name: '最大值' },
|
||
{ type: 'min', name: '最小值' }
|
||
]
|
||
},
|
||
markLine: {
|
||
data: [{ type: 'average', name: '平均值' }]
|
||
},
|
||
lineStyle: {
|
||
color: 'rgb(247, 184, 81)'
|
||
},
|
||
itemStyle: {
|
||
color: 'rgb(247, 184, 81)'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgb(247, 184, 81)'
|
||
}
|
||
},
|
||
{
|
||
name: chartType.value == 'net' ? '接收' : '写入',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: chartType.value == 'net' ? netBytesRecv.value : diskWriteBytes.value,
|
||
markPoint: {
|
||
data: [
|
||
{ type: 'max', name: '最大值' },
|
||
{ type: 'min', name: '最小值' }
|
||
]
|
||
},
|
||
markLine: {
|
||
data: [{ type: 'average', name: '平均值' }]
|
||
},
|
||
lineStyle: {
|
||
color: 'rgb(82, 169, 255)'
|
||
},
|
||
itemStyle: {
|
||
color: 'rgb(82, 169, 255)'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgb(82, 169, 255)'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
})
|
||
|
||
let isFetching = false
|
||
|
||
const fetchCurrent = async () => {
|
||
if (isFetching) return
|
||
isFetching = true
|
||
dashboard
|
||
.current(nets.value, disks.value)
|
||
.then(({ data }) => {
|
||
data.percent = formatPercent(data.percent)
|
||
data.mem.usedPercent = formatPercent(data.mem.usedPercent)
|
||
// 计算 CPU 核心数
|
||
if (cores.value == 0) {
|
||
for (let i = 0; i < data.cpus.length; i++) {
|
||
cores.value += data.cpus[i].cores
|
||
}
|
||
}
|
||
// 计算实时数据
|
||
const time = current.time == 0 ? 3 : toTimestamp(data.time) - current.time
|
||
let netTotalSentTemp = 0
|
||
let netTotalRecvTemp = 0
|
||
for (let i = 0; i < data.net.length; i++) {
|
||
if (data.net[i].name === 'lo') {
|
||
continue
|
||
}
|
||
netTotalSentTemp += data.net[i].bytesSent
|
||
netTotalRecvTemp += data.net[i].bytesRecv
|
||
}
|
||
current.netBytesSent =
|
||
total.netBytesSent != 0 ? (netTotalSentTemp - total.netBytesSent) / time : 0
|
||
current.netBytesRecv =
|
||
total.netBytesRecv != 0 ? (netTotalRecvTemp - total.netBytesRecv) / time : 0
|
||
total.netBytesSent = netTotalSentTemp
|
||
total.netBytesRecv = netTotalRecvTemp
|
||
// 计算硬盘读写
|
||
let diskTotalReadTemp = 0
|
||
let diskTotalWriteTemp = 0
|
||
let diskRWTimeTemp = 0
|
||
for (let i = 0; i < data.disk_io.length; i++) {
|
||
diskTotalReadTemp += data.disk_io[i].readBytes
|
||
diskTotalWriteTemp += data.disk_io[i].writeBytes
|
||
diskRWTimeTemp += data.disk_io[i].readTime + data.disk_io[i].writeTime
|
||
}
|
||
current.diskReadBytes =
|
||
total.diskReadBytes != 0 ? (diskTotalReadTemp - total.diskReadBytes) / time : 0
|
||
current.diskWriteBytes =
|
||
total.diskWriteBytes != 0 ? (diskTotalWriteTemp - total.diskWriteBytes) / time : 0
|
||
current.diskRWBytes =
|
||
total.diskRWBytes != 0
|
||
? (diskTotalReadTemp + diskTotalWriteTemp - total.diskRWBytes) / time
|
||
: 0
|
||
current.diskRWTime =
|
||
total.diskRWTime != 0 ? Number(((diskRWTimeTemp - total.diskRWTime) / time).toFixed(2)) : 0
|
||
current.time = toTimestamp(data.time)
|
||
total.diskReadBytes = diskTotalReadTemp
|
||
total.diskWriteBytes = diskTotalWriteTemp
|
||
total.diskRWBytes = diskTotalReadTemp + diskTotalWriteTemp
|
||
total.diskRWTime = diskRWTimeTemp
|
||
|
||
// 图表数据填充
|
||
netBytesSent.value.push(calculateSize(current.netBytesSent))
|
||
if (netBytesSent.value.length > 10) {
|
||
netBytesSent.value.splice(0, 1)
|
||
}
|
||
netBytesRecv.value.push(calculateSize(current.netBytesRecv))
|
||
if (netBytesRecv.value.length > 10) {
|
||
netBytesRecv.value.splice(0, 1)
|
||
}
|
||
diskReadBytes.value.push(calculateSize(current.diskReadBytes))
|
||
if (diskReadBytes.value.length > 10) {
|
||
diskReadBytes.value.splice(0, 1)
|
||
}
|
||
diskWriteBytes.value.push(calculateSize(current.diskWriteBytes))
|
||
if (diskWriteBytes.value.length > 10) {
|
||
diskWriteBytes.value.splice(0, 1)
|
||
}
|
||
timeDiskData.value.push(formatDateTime(data.time))
|
||
if (timeDiskData.value.length > 10) {
|
||
timeDiskData.value.splice(0, 1)
|
||
}
|
||
timeNetData.value.push(formatDateTime(data.time))
|
||
if (timeNetData.value.length > 10) {
|
||
timeNetData.value.splice(0, 1)
|
||
}
|
||
|
||
realtime.value = data
|
||
})
|
||
.finally(() => {
|
||
isFetching = false
|
||
})
|
||
}
|
||
|
||
const fetchSystemInfo = async () => {
|
||
dashboard.systemInfo().then((res) => {
|
||
systemInfo.value = res.data
|
||
})
|
||
}
|
||
|
||
const fetchCountInfo = async () => {
|
||
dashboard.countInfo().then((res) => {
|
||
countInfo.value = res.data
|
||
})
|
||
}
|
||
|
||
const fetchHomeApps = async () => {
|
||
homeAppsLoading.value = true
|
||
dashboard.homeApps().then((res) => {
|
||
homeApps.value = res.data
|
||
homeAppsLoading.value = false
|
||
})
|
||
}
|
||
|
||
const handleRestartPanel = () => {
|
||
clearInterval(homeInterval)
|
||
window.$message.loading('面板重启中...')
|
||
dashboard.restart().then(() => {
|
||
window.$message.success('面板重启成功')
|
||
setTimeout(() => {
|
||
tabStore.reloadTab(tabStore.active)
|
||
}, 3000)
|
||
})
|
||
}
|
||
|
||
const handleUpdate = () => {
|
||
dashboard.checkUpdate().then((res) => {
|
||
if (res.data.update) {
|
||
router.push({ name: 'dashboard-update' })
|
||
} else {
|
||
window.$message.success('当前已是最新版本')
|
||
}
|
||
})
|
||
}
|
||
|
||
const toSponsor = () => {
|
||
window.open('https://afdian.com/a/TheTNB')
|
||
}
|
||
|
||
const handleManageApp = (slug: string) => {
|
||
router.push({ name: 'apps-' + slug + '-index' })
|
||
}
|
||
|
||
const calculateSize = (bytes: any) => {
|
||
switch (unitType.value) {
|
||
case 'B':
|
||
return Number(bytes.toFixed(2))
|
||
case 'KB':
|
||
return Number((bytes / 1024).toFixed(2))
|
||
case 'MB':
|
||
return Number((bytes / 1024 / 1024).toFixed(2))
|
||
case 'GB':
|
||
return Number((bytes / 1024 / 1024 / 1024).toFixed(2))
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
const clearCurrent = () => {
|
||
total.netBytesSent = 0
|
||
total.netBytesRecv = 0
|
||
total.diskReadBytes = 0
|
||
total.diskWriteBytes = 0
|
||
total.diskRWBytes = 0
|
||
total.diskRWTime = 0
|
||
netBytesSent.value = []
|
||
netBytesRecv.value = []
|
||
timeNetData.value = []
|
||
diskReadBytes.value = []
|
||
diskWriteBytes.value = []
|
||
timeDiskData.value = []
|
||
}
|
||
|
||
const quantifier = computed(() => {
|
||
return locale.value === 'en' ? '' : ' 个'
|
||
})
|
||
|
||
let homeInterval: any = null
|
||
|
||
onMounted(() => {
|
||
fetchCurrent()
|
||
fetchSystemInfo()
|
||
fetchCountInfo()
|
||
fetchHomeApps()
|
||
homeInterval = setInterval(() => {
|
||
fetchCurrent()
|
||
}, 3000)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
clearInterval(homeInterval)
|
||
})
|
||
|
||
if (import.meta.hot) {
|
||
import.meta.hot.accept()
|
||
import.meta.hot.dispose(() => {
|
||
clearInterval(homeInterval)
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<AppPage :show-footer="true" min-w-375>
|
||
<div flex-1>
|
||
<n-space vertical>
|
||
<n-card :segmented="true" rounded-10 size="small">
|
||
<n-page-header :subtitle="systemInfo?.panel_version">
|
||
<n-grid :cols="4" pb-10>
|
||
<n-gi>
|
||
<n-statistic label="网站" :value="countInfo.website + quantifier" />
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-statistic label="数据库" :value="countInfo.database + quantifier" />
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-statistic label="FTP" :value="countInfo.ftp + quantifier" />
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-statistic label="计划任务" :value="countInfo.cron + quantifier" />
|
||
</n-gi>
|
||
</n-grid>
|
||
<template #title>耗子面板</template>
|
||
<template #extra>
|
||
<n-flex>
|
||
<n-button type="primary" @click="toSponsor"> 赞助支持 </n-button>
|
||
<n-popconfirm @positive-click="handleRestartPanel">
|
||
<template #trigger>
|
||
<n-button type="warning"> 重启 </n-button>
|
||
</template>
|
||
确定要重启面板吗?
|
||
</n-popconfirm>
|
||
<n-button type="success" @click="handleUpdate"> 更新 </n-button>
|
||
</n-flex>
|
||
</template>
|
||
</n-page-header>
|
||
</n-card>
|
||
|
||
<n-card :segmented="true" rounded-10 size="small" title="资源总览">
|
||
<n-flex v-if="realtime" size="large">
|
||
<n-popover trigger="hover">
|
||
<template #trigger>
|
||
<n-flex vertical flex items-center p-20 pl-40 pr-40>
|
||
<p>负载状态</p>
|
||
<n-progress
|
||
type="dashboard"
|
||
:percentage="Math.round(formatPercent((realtime.load.load1 / cores) * 100))"
|
||
:color="statusColor((realtime.load.load1 / cores) * 100)"
|
||
>
|
||
</n-progress>
|
||
<p>{{ statusText((realtime.load.load1 / cores) * 100) }}</p>
|
||
</n-flex>
|
||
</template>
|
||
<n-table :single-line="false" striped>
|
||
<tr>
|
||
<th>最近 1 分钟</th>
|
||
<td>
|
||
{{ formatPercent((realtime.load.load1 / cores) * 100) }}% /
|
||
{{ realtime.load.load1 }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>最近 5 分钟</th>
|
||
<td>
|
||
{{ formatPercent((realtime.load.load5 / cores) * 100) }}% /
|
||
{{ realtime.load.load5 }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>最近 15 分钟</th>
|
||
<td>
|
||
{{ formatPercent((realtime.load.load15 / cores) * 100) }}% /
|
||
{{ realtime.load.load15 }}
|
||
</td>
|
||
</tr>
|
||
</n-table>
|
||
</n-popover>
|
||
<n-popover trigger="hover">
|
||
<template #trigger>
|
||
<n-flex vertical flex items-center p-20 pl-40 pr-40>
|
||
<p>CPU</p>
|
||
<n-progress
|
||
type="dashboard"
|
||
:percentage="realtime.percent"
|
||
:color="statusColor(realtime.percent)"
|
||
>
|
||
</n-progress>
|
||
<p>{{ cores }} 核心</p>
|
||
</n-flex>
|
||
</template>
|
||
<n-table :single-line="false" striped>
|
||
<tr>
|
||
<th>型号</th>
|
||
<td>{{ realtime.cpus[0].modelName }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>参数</th>
|
||
<td>
|
||
{{ realtime.cpus.length }} CPU {{ cores }} 核心
|
||
{{ formatBytes(realtime.cpus[0].cacheSize * 1024) }} 缓存
|
||
</td>
|
||
</tr>
|
||
<tr v-for="item in realtime.cpus" :key="item.modelName">
|
||
<th>CPU-{{ item.cpu }}</th>
|
||
<td>
|
||
使用率 {{ formatPercent(realtime.percents[item.cpu]) }}% 频率 {{ item.mhz }} MHz
|
||
</td>
|
||
</tr>
|
||
</n-table>
|
||
</n-popover>
|
||
<n-popover trigger="hover">
|
||
<template #trigger>
|
||
<n-flex vertical flex items-center p-20 pl-40 pr-40>
|
||
<p>内存</p>
|
||
<n-progress
|
||
type="dashboard"
|
||
:percentage="realtime.mem.usedPercent"
|
||
:color="statusColor(realtime.mem.usedPercent)"
|
||
>
|
||
</n-progress>
|
||
<p>{{ formatBytes(realtime.mem.total) }}</p>
|
||
</n-flex>
|
||
</template>
|
||
<n-table :single-line="false" striped>
|
||
<tr>
|
||
<th>活跃</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.active) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>不活跃</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.inactive) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>空闲</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.free) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>共享</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.shared) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>已提交</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.committedas) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>提交限制</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.commitlimit) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>SWAP大小</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.swaptotal) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>SWAP已用</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.swapcached) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>SWAP可用</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.swapfree) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>物理内存大小</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.total) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>物理内存已用</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.used) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>物理内存可用</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.available) }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>buffers/cached</th>
|
||
<td>
|
||
{{ formatBytes(realtime.mem.buffers) }} / {{ formatBytes(realtime.mem.cached) }}
|
||
</td>
|
||
</tr>
|
||
</n-table>
|
||
</n-popover>
|
||
<n-popover v-for="item in realtime.disk_usage" :key="item.path" trigger="hover">
|
||
<template #trigger>
|
||
<n-flex vertical flex items-center p-20 pl-40 pr-40>
|
||
<p>{{ item.path }}</p>
|
||
<n-progress
|
||
type="dashboard"
|
||
:percentage="formatPercent(item.usedPercent)"
|
||
:color="statusColor(item.usedPercent)"
|
||
>
|
||
</n-progress>
|
||
<p>{{ formatBytes(item.used) }} / {{ formatBytes(item.total) }}</p>
|
||
</n-flex>
|
||
</template>
|
||
<n-table :single-line="false">
|
||
<tr>
|
||
<th>挂载点</th>
|
||
<td>{{ item.path }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>文件系统</th>
|
||
<td>{{ item.fstype }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Inodes 使用率</th>
|
||
<td>{{ formatPercent(item.inodesUsedPercent) }}%</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Inodes 总数</th>
|
||
<td>{{ item.inodesTotal }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Inodes 已用</th>
|
||
<td>{{ item.inodesUsed }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Inodes 可用</th>
|
||
<td>{{ item.inodesFree }}</td>
|
||
</tr>
|
||
</n-table>
|
||
</n-popover>
|
||
</n-flex>
|
||
<n-skeleton v-else text :repeat="10" />
|
||
</n-card>
|
||
<n-grid
|
||
x-gap="12"
|
||
y-gap="12"
|
||
cols="1 s:1 m:1 l:2 xl:2 2xl:2"
|
||
item-responsive
|
||
responsive="screen"
|
||
>
|
||
<n-gi>
|
||
<n-flex vertical>
|
||
<n-card :segmented="true" size="small" title="快捷应用" min-h-340 rounded-10>
|
||
<n-scrollbar max-h-270>
|
||
<n-grid
|
||
v-if="!homeAppsLoading"
|
||
x-gap="12"
|
||
y-gap="12"
|
||
cols="3 s:1 m:2 l:3"
|
||
item-responsive
|
||
responsive="screen"
|
||
>
|
||
<n-gi v-for="item in homeApps" :key="item.name">
|
||
<n-card
|
||
:segmented="true"
|
||
size="small"
|
||
cursor-pointer
|
||
rounded-10
|
||
hover:card-shadow
|
||
@click="handleManageApp(item.slug)"
|
||
>
|
||
<n-space>
|
||
<n-thing>
|
||
<template #avatar>
|
||
<div class="mt-8">
|
||
<TheIcon
|
||
:size="30"
|
||
:icon="item.icon"
|
||
color="var(--primary-color)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #header>
|
||
{{ item.name }}
|
||
</template>
|
||
<template #description>
|
||
{{ item.version }}
|
||
</template>
|
||
</n-thing>
|
||
</n-space>
|
||
</n-card>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-scrollbar>
|
||
<n-text v-if="!homeAppsLoading && !homeApps">
|
||
您还没有设置任何应用在此显示!
|
||
</n-text>
|
||
<n-skeleton v-if="homeAppsLoading" text :repeat="12" />
|
||
</n-card>
|
||
<n-card :segmented="true" rounded-10 size="small" title="环境信息">
|
||
<n-table v-if="systemInfo" :single-line="false">
|
||
<tr>
|
||
<th>系统主机名</th>
|
||
<td>
|
||
{{ systemInfo?.hostname || '加载中...' }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>系统版本号</th>
|
||
<td>
|
||
{{ `${systemInfo?.os_name} ${systemInfo?.kernel_arch}` || '加载中...' }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>系统内核版本</th>
|
||
<td>
|
||
{{ systemInfo?.kernel_version || '加载中...' }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>系统运行时间</th>
|
||
<td>
|
||
{{ formatDuration(Number(systemInfo?.uptime)) || '加载中...' }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>面板内部版本</th>
|
||
<td>
|
||
{{
|
||
systemInfo?.commit_hash +
|
||
' ' +
|
||
systemInfo?.go_version +
|
||
' ' +
|
||
systemInfo?.build_time || '加载中...'
|
||
}}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>面板编译信息</th>
|
||
<td>
|
||
{{
|
||
systemInfo?.build_id +
|
||
' ' +
|
||
systemInfo?.build_user +
|
||
'/' +
|
||
systemInfo?.build_host || '加载中...'
|
||
}}
|
||
</td>
|
||
</tr>
|
||
</n-table>
|
||
<n-skeleton v-else text :repeat="9" />
|
||
</n-card>
|
||
</n-flex>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-card :segmented="true" rounded-10 size="small" title="实时监控">
|
||
<n-flex vertical v-if="systemInfo">
|
||
<n-form
|
||
inline
|
||
label-placement="left"
|
||
label-width="auto"
|
||
require-mark-placement="right-hanging"
|
||
>
|
||
<n-form-item>
|
||
<n-radio-group v-model:value="chartType">
|
||
<n-radio-button value="net" label="网络" />
|
||
<n-radio-button value="disk" label="硬盘" />
|
||
</n-radio-group>
|
||
</n-form-item>
|
||
<n-form-item label="单位" ml-auto>
|
||
<n-select
|
||
v-model:value="unitType"
|
||
:options="units"
|
||
@update-value="clearCurrent"
|
||
w-80
|
||
></n-select>
|
||
</n-form-item>
|
||
<n-form-item v-if="chartType == 'net'" label="网卡">
|
||
<n-select
|
||
multiple
|
||
v-model:value="nets"
|
||
:options="systemInfo.nets"
|
||
@update-value="clearCurrent"
|
||
w-200
|
||
></n-select>
|
||
</n-form-item>
|
||
<n-form-item v-if="chartType == 'disk'" label="硬盘">
|
||
<n-select
|
||
multiple
|
||
v-model:value="disks"
|
||
:options="systemInfo.disks"
|
||
@update-value="clearCurrent"
|
||
w-200
|
||
></n-select>
|
||
</n-form-item>
|
||
</n-form>
|
||
<n-flex v-if="chartType == 'net'">
|
||
<n-tag>总发送 {{ formatBytes(total.netBytesSent) }}</n-tag>
|
||
<n-tag>总接收 {{ formatBytes(total.netBytesRecv) }}</n-tag>
|
||
<n-tag>实时发送 {{ formatBytes(current.netBytesSent) }}/s</n-tag>
|
||
<n-tag>实时接收 {{ formatBytes(current.netBytesRecv) }}/s</n-tag>
|
||
</n-flex>
|
||
<n-flex v-if="chartType == 'disk'">
|
||
<n-tag>读取 {{ formatBytes(total.diskReadBytes) }}</n-tag>
|
||
<n-tag>写入 {{ formatBytes(total.diskWriteBytes) }}</n-tag>
|
||
<n-tag>实时读写 {{ formatBytes(current.diskRWBytes) }}/s</n-tag>
|
||
<n-tag>读写延迟 {{ current.diskRWTime }}ms</n-tag>
|
||
</n-flex>
|
||
<n-card :bordered="false" h-530 pt-10>
|
||
<v-chart class="chart" :option="chartOptions" autoresize />
|
||
</n-card>
|
||
</n-flex>
|
||
<n-skeleton v-else text :repeat="25" />
|
||
</n-card>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-space>
|
||
</div>
|
||
</AppPage>
|
||
</template>
|