A:增加主页面数据展示
This commit is contained in:
1 parent
475fcdd2c0
commit
2f13d6a643
12 files changed
+1723
-43
No files matched your search
@@ -8,7 +8,9 @@ import { ref, onMounted, provide } from 'vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { NConfigProvider, NGlobalStyle } from 'naive-ui'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import MainLayout from '@/layouts/MainLayout.vue'
|
||||
import NetworkConnectionPanel1 from '@/components/NetworkConnectionPanel1.vue'
|
||||
import MainLayout from './layouts/MainLayout.vue'
|
||||
import NetworkConnectionPanel2 from '@/components/NetworkConnectionPanel2.vue'
|
||||
|
||||
const router = useRouter()
|
||||
// 是否展示生成路由
|
||||
@@ -132,6 +134,10 @@ onMounted(() => {
|
||||
window.removeEventListener('mouseup', endDrag)
|
||||
}
|
||||
})
|
||||
|
||||
const getLayout = (route) => {
|
||||
return route.meta.layout === 'main' ? MainLayout : 'div' // 'div' 表示没有布局
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -167,13 +173,14 @@ onMounted(() => {
|
||||
|
||||
<!-- 展示主要内容 -->
|
||||
<div class="main-content">
|
||||
<MainLayout>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<component :is="getLayout(route)">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</MainLayout>
|
||||
</component>
|
||||
</transition>
|
||||
</router-view>
|
||||
<!-- <network-connection-panel2/>-->
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,1391 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="process-connection-panel" :class="sizeClass">
|
||||
<div class="panel-container">
|
||||
<!-- 进程概览统计 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-show="showStatCard('process')">
|
||||
<div class="stat-icon-wrapper process">
|
||||
<n-icon :component="DesktopOutline" :size="statIconSize" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.activeProcesses }}</div>
|
||||
<div class="stat-label">活跃进程 / {{ stats.totalProcesses }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" v-show="showStatCard('connection')">
|
||||
<div class="stat-icon-wrapper connection">
|
||||
<n-icon :component="GitNetworkOutline" :size="statIconSize" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.activeConnections }}</div>
|
||||
<div class="stat-label">活跃连接 / {{ stats.totalConnections }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" v-show="showStatCard('upload')">
|
||||
<div class="stat-icon-wrapper upload">
|
||||
<n-icon :component="ArrowUpOutline" :size="statIconSize" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ formatSpeed(stats.totalUploadSpeed) }}</div>
|
||||
<div class="stat-label">总上传速度</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" v-show="showStatCard('download')">
|
||||
<div class="stat-icon-wrapper download">
|
||||
<n-icon :component="ArrowDownOutline" :size="statIconSize" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ formatSpeed(stats.totalDownloadSpeed) }}</div>
|
||||
<div class="stat-label">总下载速度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进程列表 -->
|
||||
<div class="process-list">
|
||||
<div
|
||||
v-for="process in processData"
|
||||
:key="process.processId"
|
||||
class="process-item"
|
||||
:class="{
|
||||
'is-expanded': expandedItems.has(process.processId),
|
||||
'is-exited': process.hasExited,
|
||||
}"
|
||||
>
|
||||
<!-- 进程头部 -->
|
||||
<div class="process-header" @click="toggleExpand(process.processId)">
|
||||
<div class="header-left">
|
||||
<div class="expand-indicator">
|
||||
<n-icon
|
||||
:component="ChevronForwardOutline"
|
||||
:size="expandIconSize"
|
||||
class="expand-arrow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="process-info">
|
||||
<div class="process-title">
|
||||
<h4 class="process-name">{{ process.processName }}</h4>
|
||||
<n-tag :type="getProcessStatus(process).type" :size="tagSize" round>
|
||||
{{ getProcessStatus(process).text }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-if="process.hasExited && process.exitCode !== undefined && !isCompact"
|
||||
:type="process.exitCode === 0 ? 'success' : 'error'"
|
||||
:size="tagSize"
|
||||
>
|
||||
退出代码: {{ process.exitCode }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="process-details">
|
||||
<span class="detail-item primary">
|
||||
<n-icon :component="DesktopOutline" :size="detailIconSize" />
|
||||
PID: {{ process.processId }}
|
||||
</span>
|
||||
<span class="detail-item" v-show="showProcessDetail('time')">
|
||||
<n-icon :component="TimeOutline" :size="detailIconSize" />
|
||||
{{ process.hasExited ? '运行时长' : '已运行' }}:
|
||||
{{ formatDuration(process.startTime, process.exitTime) }}
|
||||
</span>
|
||||
<span class="detail-item" v-show="showProcessDetail('thread')">
|
||||
<n-icon :component="PeopleOutline" :size="detailIconSize" />
|
||||
{{ process.threadCount }} 线程
|
||||
</span>
|
||||
<span class="detail-item" v-show="showProcessDetail('memory')">
|
||||
<n-icon :component="HardwareChipOutline" :size="detailIconSize" />
|
||||
内存: {{ formatBytes(process.useMemory) }}
|
||||
</span>
|
||||
<span class="detail-item primary">
|
||||
<n-icon :component="GitNetworkOutline" :size="detailIconSize" />
|
||||
{{ process.connections.length }} 个连接
|
||||
</span>
|
||||
<span
|
||||
v-if="process.mainModuleName && showProcessDetail('module')"
|
||||
class="detail-item"
|
||||
:title="process.mainModulePath"
|
||||
>
|
||||
<n-icon :component="FileTrayFullOutline" :size="detailIconSize" />
|
||||
{{ process.mainModuleName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="traffic-summary">
|
||||
<div class="traffic-speed">
|
||||
<div class="traffic-item upload">
|
||||
<n-icon :component="ArrowUpOutline" :size="trafficIconSize" />
|
||||
<span>{{ formatSpeed(process.uploadSpeed) }}</span>
|
||||
</div>
|
||||
<div class="traffic-item download">
|
||||
<n-icon :component="ArrowDownOutline" :size="trafficIconSize" />
|
||||
<span>{{ formatSpeed(process.downloadSpeed) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-total" v-if="!isCompact">
|
||||
<span
|
||||
>总计: ↑{{ formatBytes(process.totalUploaded) }} ↓{{
|
||||
formatBytes(process.totalDownloaded)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接列表 -->
|
||||
<div class="connections-content">
|
||||
<div class="content-wrapper">
|
||||
<div class="connections-header">
|
||||
<h5 class="connections-title">网络连接详情</h5>
|
||||
</div>
|
||||
|
||||
<n-empty
|
||||
v-if="process.connections.length === 0"
|
||||
description="没有网络连接"
|
||||
:style="{ padding: isCompact ? '12px' : '20px' }"
|
||||
/>
|
||||
|
||||
<div v-else class="connection-list">
|
||||
<div
|
||||
v-for="connection in process.connections"
|
||||
:key="connection.connectionKey"
|
||||
class="connection-item"
|
||||
:class="{ 'is-active': connection.isActive }"
|
||||
>
|
||||
<div class="connection-main">
|
||||
<div class="connection-status">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="connection.isActive ? 'active' : 'inactive'"
|
||||
:title="connection.isActive ? '活跃' : '空闲'"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="connection-info">
|
||||
<div class="connection-tags">
|
||||
<n-tag
|
||||
:type="getConnectionStateType(connection.state, connection.isActive)"
|
||||
:size="tagSize"
|
||||
>
|
||||
{{ connection.state }}
|
||||
</n-tag>
|
||||
<n-tag :size="tagSize">{{ connection.protocol }}</n-tag>
|
||||
<n-tag :size="tagSize" v-if="!isCompact">
|
||||
<n-icon :component="getDirectionIcon(connection.direction)" size="12" />
|
||||
{{ getDirectionText(connection.direction) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="connection-addresses">
|
||||
<div class="address-section">
|
||||
<n-icon :component="LocationOutline" :size="addressIconSize" />
|
||||
<span class="address-value">
|
||||
{{ formatEndpoint(connection.localEndpoint) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="connection-arrow">
|
||||
<n-icon :component="ArrowForwardOutline" :size="arrowIconSize" />
|
||||
</div>
|
||||
<div class="address-section">
|
||||
<n-icon :component="GlobeOutline" :size="addressIconSize" />
|
||||
<span class="address-value">
|
||||
{{ formatEndpoint(connection.remoteEndpoint) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-meta" v-if="!isCompact">
|
||||
<span class="meta-item">
|
||||
<n-icon :component="TimeOutline" size="12" />
|
||||
持续: {{ formatDuration(connection.startTime) }}
|
||||
</span>
|
||||
<span v-if="!connection.isActive" class="meta-item inactive">
|
||||
<n-icon :component="PauseCircleOutline" size="12" />
|
||||
最后活跃: {{ getTimeSinceActive(connection.lastActiveTime) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-side">
|
||||
<div class="connection-traffic">
|
||||
<div class="traffic-stats">
|
||||
<div class="traffic-row upload">
|
||||
<n-icon :component="ArrowUpOutline" :size="trafficIconSize" />
|
||||
<span class="traffic-value">{{
|
||||
formatSpeed(connection.currentSendSpeed)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="traffic-row download">
|
||||
<n-icon :component="ArrowDownOutline" :size="trafficIconSize" />
|
||||
<span class="traffic-value">{{
|
||||
formatSpeed(connection.currentReceiveSpeed)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-total-small" v-if="!isCompact">
|
||||
总计: ↑{{ formatBytes(connection.bytesSent) }} ↓{{
|
||||
formatBytes(connection.bytesReceived)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-actions" v-if="!isCompact">
|
||||
<n-button size="tiny" circle quaternary type="info" title="查看详情">
|
||||
<template #icon>
|
||||
<n-icon :component="InformationCircleOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="tiny" circle quaternary type="warning" title="断开连接">
|
||||
<template #icon>
|
||||
<n-icon :component="StopCircleOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { NButton, NIcon, NTag, NEmpty } from 'naive-ui'
|
||||
import {
|
||||
DesktopOutline,
|
||||
GitNetworkOutline,
|
||||
ArrowUpOutline,
|
||||
ArrowDownOutline,
|
||||
ChevronForwardOutline,
|
||||
TimeOutline,
|
||||
HardwareChipOutline,
|
||||
LocationOutline,
|
||||
GlobeOutline,
|
||||
ArrowForwardOutline,
|
||||
InformationCircleOutline,
|
||||
StopCircleOutline,
|
||||
PeopleOutline,
|
||||
FileTrayFullOutline,
|
||||
PauseCircleOutline,
|
||||
SwapHorizontalOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
// 类型定义
|
||||
type ProtocolType = 'TCP' | 'UDP' | 'ICMP' | 'Other'
|
||||
type ConnectionState =
|
||||
| 'ESTABLISHED'
|
||||
| 'LISTEN'
|
||||
| 'TIME_WAIT'
|
||||
| 'CLOSE_WAIT'
|
||||
| 'SYN_SENT'
|
||||
| 'SYN_RECV'
|
||||
| 'CLOSING'
|
||||
| 'CLOSED'
|
||||
type TrafficDirection = 'Inbound' | 'Outbound' | 'Both'
|
||||
type SizeType = 'compact' | 'normal' | 'comfortable' | 'spacious'
|
||||
|
||||
interface IPEndPoint {
|
||||
address: string
|
||||
port: number
|
||||
}
|
||||
|
||||
interface ConnectionInfo {
|
||||
connectionKey: string
|
||||
processId: number
|
||||
protocol: ProtocolType
|
||||
localEndpoint: IPEndPoint
|
||||
remoteEndpoint: IPEndPoint
|
||||
state: ConnectionState
|
||||
direction: TrafficDirection
|
||||
startTime: Date
|
||||
lastActiveTime: Date
|
||||
bytesSent: number
|
||||
bytesReceived: number
|
||||
currentSendSpeed: number
|
||||
currentReceiveSpeed: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
interface ProcessType {
|
||||
processName: string
|
||||
processId: number
|
||||
startTime: Date
|
||||
hasExited: boolean
|
||||
exitTime?: Date
|
||||
exitCode?: number
|
||||
useMemory: number
|
||||
threadCount: number
|
||||
mainModulePath?: string
|
||||
mainModuleName?: string
|
||||
totalUploaded: number
|
||||
totalDownloaded: number
|
||||
uploadSpeed: number
|
||||
downloadSpeed: number
|
||||
connections: ConnectionInfo[]
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
softwareName?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
softwareName: '未知应用',
|
||||
})
|
||||
|
||||
// 响应式相关
|
||||
const containerRef = ref<HTMLElement>()
|
||||
const containerWidth = ref(1200)
|
||||
const resizeObserver = ref<ResizeObserver>()
|
||||
const actualSizeType = ref<SizeType>('spacious')
|
||||
|
||||
// 添加防抖处理
|
||||
let resizeTimeout: number | undefined
|
||||
|
||||
// 使用滞后(hysteresis)机制计算尺寸类型
|
||||
const calculateSizeType = (width: number): SizeType => {
|
||||
const currentType = actualSizeType.value
|
||||
const HYSTERESIS = 20 // 滞后阈值,防止在临界值附近抖动
|
||||
|
||||
// 根据当前状态和新宽度计算新的类型
|
||||
// 使用不同的阈值来避免频繁切换
|
||||
if (currentType === 'compact') {
|
||||
if (width > 500 + HYSTERESIS) return 'normal'
|
||||
} else if (currentType === 'normal') {
|
||||
if (width < 500 - HYSTERESIS) return 'compact'
|
||||
if (width > 800 + HYSTERESIS) return 'comfortable'
|
||||
} else if (currentType === 'comfortable') {
|
||||
if (width < 800 - HYSTERESIS) return 'normal'
|
||||
if (width > 1200 + HYSTERESIS) return 'spacious'
|
||||
} else {
|
||||
// spacious
|
||||
if (width < 1200 - HYSTERESIS) return 'comfortable'
|
||||
}
|
||||
|
||||
// 如果没有跨越阈值,保持当前状态
|
||||
return currentType
|
||||
}
|
||||
|
||||
// 使用计算属性而不是直接响应宽度变化
|
||||
const sizeType = computed(() => actualSizeType.value)
|
||||
|
||||
const sizeClass = computed(() => `size-${sizeType.value}`)
|
||||
const isCompact = computed(() => sizeType.value === 'compact')
|
||||
const isNormal = computed(() => sizeType.value === 'normal')
|
||||
const isComfortable = computed(() => sizeType.value === 'comfortable')
|
||||
|
||||
// 响应式图标大小
|
||||
const statIconSize = computed(() => {
|
||||
switch (sizeType.value) {
|
||||
case 'compact':
|
||||
return 20
|
||||
case 'normal':
|
||||
return 22
|
||||
default:
|
||||
return 24
|
||||
}
|
||||
})
|
||||
|
||||
const expandIconSize = computed(() => (isCompact.value ? 14 : 16))
|
||||
const detailIconSize = computed(() => (isCompact.value ? 12 : 14))
|
||||
const trafficIconSize = computed(() => (isCompact.value ? 12 : 14))
|
||||
const addressIconSize = computed(() => (isCompact.value ? 10 : 12))
|
||||
const arrowIconSize = computed(() => (isCompact.value ? 12 : 14))
|
||||
|
||||
const tagSize = computed(() => {
|
||||
switch (sizeType.value) {
|
||||
case 'compact':
|
||||
return 'tiny' as const
|
||||
case 'normal':
|
||||
return 'small' as const
|
||||
default:
|
||||
return 'small' as const
|
||||
}
|
||||
})
|
||||
|
||||
// 根据尺寸决定是否显示某些元素
|
||||
const showStatCard = (type: string) => {
|
||||
if (sizeType.value === 'spacious') return true
|
||||
if (sizeType.value === 'comfortable') return true
|
||||
if (sizeType.value === 'normal') return type !== 'download'
|
||||
return type === 'process' || type === 'connection'
|
||||
}
|
||||
|
||||
const showProcessDetail = (type: string) => {
|
||||
if (sizeType.value === 'spacious') return true
|
||||
if (sizeType.value === 'comfortable') return type !== 'module'
|
||||
if (sizeType.value === 'normal') return type === 'time' || type === 'memory'
|
||||
return false
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const expandedItems = ref(new Set<number>([1234]))
|
||||
|
||||
// 模拟数据
|
||||
const processData = ref<ProcessType[]>([
|
||||
{
|
||||
processName: 'Chrome',
|
||||
processId: 1234,
|
||||
startTime: new Date(Date.now() - 8136000),
|
||||
hasExited: false,
|
||||
useMemory: 256901120,
|
||||
threadCount: 23,
|
||||
mainModulePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
mainModuleName: 'Google Chrome',
|
||||
totalUploaded: 2202624,
|
||||
totalDownloaded: 16056832,
|
||||
uploadSpeed: 2355,
|
||||
downloadSpeed: 16076,
|
||||
connections: [
|
||||
{
|
||||
connectionKey: 'tcp-1234-1',
|
||||
processId: 1234,
|
||||
protocol: 'TCP',
|
||||
localEndpoint: { address: '192.168.1.100', port: 54321 },
|
||||
remoteEndpoint: { address: '142.250.191.78', port: 443 },
|
||||
state: 'ESTABLISHED',
|
||||
direction: 'Both',
|
||||
startTime: new Date(Date.now() - 932000),
|
||||
lastActiveTime: new Date(Date.now() - 2000),
|
||||
bytesSent: 1258496,
|
||||
bytesReceived: 8912896,
|
||||
currentSendSpeed: 1228,
|
||||
currentReceiveSpeed: 8704,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
connectionKey: 'tcp-1234-2',
|
||||
processId: 1234,
|
||||
protocol: 'TCP',
|
||||
localEndpoint: { address: '192.168.1.100', port: 54322 },
|
||||
remoteEndpoint: { address: '151.101.193.140', port: 443 },
|
||||
state: 'TIME_WAIT',
|
||||
direction: 'Outbound',
|
||||
startTime: new Date(Date.now() - 495000),
|
||||
lastActiveTime: new Date(Date.now() - 300000),
|
||||
bytesSent: 466944,
|
||||
bytesReceived: 5452800,
|
||||
currentSendSpeed: 0,
|
||||
currentReceiveSpeed: 0,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
connectionKey: 'tcp-1234-3',
|
||||
processId: 1234,
|
||||
protocol: 'TCP',
|
||||
localEndpoint: { address: '0.0.0.0', port: 8080 },
|
||||
remoteEndpoint: { address: '0.0.0.0', port: 0 },
|
||||
state: 'LISTEN',
|
||||
direction: 'Inbound',
|
||||
startTime: new Date(Date.now() - 8136000),
|
||||
lastActiveTime: new Date(Date.now() - 8136000),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
currentSendSpeed: 0,
|
||||
currentReceiveSpeed: 0,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
processName: 'VS Code',
|
||||
processId: 5678,
|
||||
startTime: new Date(Date.now() - 5025000),
|
||||
hasExited: false,
|
||||
useMemory: 134217728,
|
||||
threadCount: 15,
|
||||
mainModulePath: '/Applications/Visual Studio Code.app/Contents/MacOS/Electron',
|
||||
mainModuleName: 'Code',
|
||||
totalUploaded: 1258496,
|
||||
totalDownloaded: 8912896,
|
||||
uploadSpeed: 819,
|
||||
downloadSpeed: 4300,
|
||||
connections: [
|
||||
{
|
||||
connectionKey: 'tcp-5678-1',
|
||||
processId: 5678,
|
||||
protocol: 'TCP',
|
||||
localEndpoint: { address: '192.168.1.100', port: 12345 },
|
||||
remoteEndpoint: { address: '140.82.114.3', port: 443 },
|
||||
state: 'ESTABLISHED',
|
||||
direction: 'Both',
|
||||
startTime: new Date(Date.now() - 5025000),
|
||||
lastActiveTime: new Date(Date.now() - 5000),
|
||||
bytesSent: 1258496,
|
||||
bytesReceived: 8912896,
|
||||
currentSendSpeed: 307,
|
||||
currentReceiveSpeed: 2150,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
processName: 'node',
|
||||
processId: 9012,
|
||||
startTime: new Date(Date.now() - 2718000),
|
||||
hasExited: true,
|
||||
exitTime: new Date(Date.now() - 300000),
|
||||
exitCode: 0,
|
||||
useMemory: 67108864,
|
||||
threadCount: 8,
|
||||
mainModulePath: '/usr/local/bin/node',
|
||||
mainModuleName: 'node',
|
||||
totalUploaded: 0,
|
||||
totalDownloaded: 0,
|
||||
uploadSpeed: 0,
|
||||
downloadSpeed: 0,
|
||||
connections: [],
|
||||
},
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const stats = computed(() => {
|
||||
const activeProcesses = processData.value.filter((p) => !p.hasExited)
|
||||
const totalConnections = processData.value.reduce((sum, p) => sum + p.connections.length, 0)
|
||||
const activeConnections = processData.value.reduce(
|
||||
(sum, p) => sum + p.connections.filter((c) => c.isActive).length,
|
||||
0,
|
||||
)
|
||||
const totalUploadSpeed = activeProcesses.reduce((sum, p) => sum + p.uploadSpeed, 0)
|
||||
const totalDownloadSpeed = activeProcesses.reduce((sum, p) => sum + p.downloadSpeed, 0)
|
||||
|
||||
return {
|
||||
activeProcesses: activeProcesses.length,
|
||||
totalProcesses: processData.value.length,
|
||||
totalConnections,
|
||||
activeConnections,
|
||||
totalUploadSpeed,
|
||||
totalDownloadSpeed,
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
// 初始化尺寸类型
|
||||
const initialWidth = containerRef.value.offsetWidth
|
||||
containerWidth.value = initialWidth
|
||||
actualSizeType.value = calculateSizeType(initialWidth)
|
||||
|
||||
// 创建 ResizeObserver,使用防抖处理
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
// 清除之前的超时
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout)
|
||||
}
|
||||
|
||||
// 设置新的超时,防抖处理
|
||||
resizeTimeout = window.setTimeout(() => {
|
||||
const newWidth = entry.contentRect.width
|
||||
containerWidth.value = newWidth
|
||||
const newType = calculateSizeType(newWidth)
|
||||
if (newType !== actualSizeType.value) {
|
||||
actualSizeType.value = newType
|
||||
}
|
||||
}, 100) // 100ms 防抖延迟
|
||||
}
|
||||
})
|
||||
|
||||
resizeObserver.value.observe(containerRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout)
|
||||
}
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const toggleExpand = (pid: number) => {
|
||||
if (expandedItems.value.has(pid)) {
|
||||
expandedItems.value.delete(pid)
|
||||
} else {
|
||||
expandedItems.value.add(pid)
|
||||
}
|
||||
}
|
||||
|
||||
const getProcessStatus = (process: ProcessType) => {
|
||||
if (process.hasExited) return { type: 'error' as const, text: '已退出' }
|
||||
if (process.connections.some((c) => c.isActive)) return { type: 'success' as const, text: '活跃' }
|
||||
return { type: 'warning' as const, text: '空闲' }
|
||||
}
|
||||
|
||||
const getConnectionStateType = (state: ConnectionState, isActive: boolean) => {
|
||||
if (!isActive) return 'default' as const
|
||||
switch (state) {
|
||||
case 'ESTABLISHED':
|
||||
return 'success' as const
|
||||
case 'LISTEN':
|
||||
return 'info' as const
|
||||
case 'TIME_WAIT':
|
||||
case 'CLOSE_WAIT':
|
||||
return 'warning' as const
|
||||
default:
|
||||
return 'default' as const
|
||||
}
|
||||
}
|
||||
|
||||
const getDirectionIcon = (direction: TrafficDirection) => {
|
||||
switch (direction) {
|
||||
case 'Inbound':
|
||||
return ArrowDownOutline
|
||||
case 'Outbound':
|
||||
return ArrowUpOutline
|
||||
case 'Both':
|
||||
return SwapHorizontalOutline
|
||||
}
|
||||
}
|
||||
|
||||
const getDirectionText = (direction: TrafficDirection) => {
|
||||
switch (direction) {
|
||||
case 'Inbound':
|
||||
return '入站'
|
||||
case 'Outbound':
|
||||
return '出站'
|
||||
case 'Both':
|
||||
return '双向'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(2))
|
||||
|
||||
// 紧凑模式下简化单位
|
||||
if (isCompact.value && i > 0) {
|
||||
return value + sizes[i][0]
|
||||
}
|
||||
|
||||
return value + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const formatSpeed = (bytesPerSecond: number): string => {
|
||||
return formatBytes(bytesPerSecond) + '/s'
|
||||
}
|
||||
|
||||
const formatDuration = (start: Date, end?: Date): string => {
|
||||
const endTime = end || new Date()
|
||||
const diff = endTime.getTime() - start.getTime()
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const minutes = Math.floor((diff % 3600000) / 60000)
|
||||
const seconds = Math.floor((diff % 60000) / 1000)
|
||||
|
||||
if (isCompact.value) {
|
||||
return `${hours}h${minutes}m`
|
||||
}
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getTimeSinceActive = (lastActive: Date): string => {
|
||||
const diff = new Date().getTime() - lastActive.getTime()
|
||||
if (diff < 1000) return '刚刚'
|
||||
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前`
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
}
|
||||
|
||||
const formatEndpoint = (endpoint: IPEndPoint): string => {
|
||||
if (isCompact.value && endpoint.address === '0.0.0.0') {
|
||||
return `*:${endpoint.port}`
|
||||
}
|
||||
return `${endpoint.address}:${endpoint.port}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.process-connection-panel {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-secondary);
|
||||
/* 使用 box-sizing 确保一致的尺寸计算 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
padding: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
/* 确保内部元素不会影响外部容器的宽度 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 响应式调整 - 保持 padding 但使用 box-sizing */
|
||||
.size-compact .panel-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.size-normal .panel-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.size-compact .stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.size-normal .stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.size-compact .stat-card {
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.size-normal .stat-card {
|
||||
padding: 16px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.size-compact .stat-icon-wrapper {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.size-normal .stat-icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.process {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.connection {
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.upload {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.download {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.size-compact .stat-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.size-normal .stat-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.size-compact .stat-label {
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.size-normal .stat-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 进程列表样式 */
|
||||
.process-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.size-compact .process-list {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.process-item {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.size-compact .process-item {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.process-item.is-exited {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.process-item:hover {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.process-item.is-expanded {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
/* 进程头部 */
|
||||
.process-header {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.size-compact .process-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.size-normal .process-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.process-item.is-expanded .process-header {
|
||||
border-bottom-color: var(--border-primary);
|
||||
background: rgba(59, 130, 246, 0.02);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.size-compact .header-left {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.expand-indicator {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.size-compact .expand-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.process-item.is-expanded .expand-indicator {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.expand-arrow {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.process-item.is-expanded .expand-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.process-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.process-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.size-compact .process-title {
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.process-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.size-compact .process-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.size-normal .process-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.process-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.size-compact .process-details {
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.size-normal .process-details {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-item.primary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.traffic-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.size-compact .traffic-summary {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.traffic-speed {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.size-compact .traffic-speed {
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.traffic-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.traffic-item.upload {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.traffic-item.download {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.traffic-total {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 连接内容区域 - 修改 max-height 动画 */
|
||||
.connections-content {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.process-item.is-expanded .connections-content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0 20px 16px 20px;
|
||||
}
|
||||
|
||||
.size-compact .content-wrapper {
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
|
||||
.size-normal .content-wrapper {
|
||||
padding: 0 16px 14px 16px;
|
||||
}
|
||||
|
||||
.connections-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.size-compact .connections-header {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.connections-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.size-compact .connections-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 连接列表 */
|
||||
.connection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.size-compact .connection-list {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.connection-item {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.size-compact .connection-item {
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.connection-item.is-active {
|
||||
background: rgba(52, 211, 153, 0.05);
|
||||
border-color: rgba(52, 211, 153, 0.2);
|
||||
}
|
||||
|
||||
.connection-item:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.connection-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.size-compact .connection-main {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.size-compact .connection-status {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.size-compact .status-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: #52c41a;
|
||||
box-shadow: 0 0 6px rgba(82, 196, 26, 0.4);
|
||||
}
|
||||
|
||||
.status-indicator.inactive {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connection-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.size-compact .connection-tags {
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.connection-addresses {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.size-compact .connection-addresses {
|
||||
font-size: 11px;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.address-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.address-value {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.connection-arrow {
|
||||
color: var(--text-quaternary);
|
||||
}
|
||||
|
||||
.connection-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-item.inactive {
|
||||
color: var(--text-quaternary);
|
||||
}
|
||||
|
||||
.connection-side {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.size-compact .connection-side {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.connection-traffic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.size-compact .connection-traffic {
|
||||
min-width: 80px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.traffic-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.size-compact .traffic-stats {
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.traffic-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.size-compact .traffic-row {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.traffic-row.upload {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.traffic-row.download {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.traffic-value {
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.size-compact .traffic-value {
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.traffic-total-small {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* CSS 变量定义 */
|
||||
:root {
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-card: rgba(255, 255, 255, 0.9);
|
||||
--bg-tertiary: #f1f5f9;
|
||||
--bg-quaternary: #e2e8f0;
|
||||
--bg-hover: #f8fafc;
|
||||
--backdrop-blur: blur(8px);
|
||||
--border-primary: #e2e8f0;
|
||||
--border-secondary: #f1f5f9;
|
||||
--border-hover: #cbd5e1;
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-muted: #64748b;
|
||||
--text-quaternary: #94a3b8;
|
||||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
--transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 暗色模式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-secondary: #0f172a;
|
||||
--bg-card: rgba(30, 41, 59, 0.9);
|
||||
--bg-tertiary: #1e293b;
|
||||
--bg-quaternary: #334155;
|
||||
--bg-hover: #1e293b;
|
||||
--border-primary: #334155;
|
||||
--border-secondary: #1e293b;
|
||||
--border-hover: #475569;
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-muted: #94a3b8;
|
||||
--text-quaternary: #64748b;
|
||||
}
|
||||
|
||||
.connection-item.is-active {
|
||||
background: rgba(52, 211, 153, 0.08);
|
||||
border-color: rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,3 +18,42 @@ export enum FILE_SIZE_UNIT_ENUM {
|
||||
GB = 'GB',
|
||||
TB = 'TB'
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态枚举
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
/** 正在尝试连接 */
|
||||
Connecting = 'Connecting',
|
||||
/** 当前连接活跃(发送/接收中) */
|
||||
Connected = 'Connected',
|
||||
/** 已断开或未活跃 */
|
||||
Disconnected = 'Disconnected',
|
||||
/** 监听状态(如本地服务器) */
|
||||
Listening = 'Listening',
|
||||
/** 无法判断状态 */
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议类型枚举
|
||||
*/
|
||||
export enum ProtocolType {
|
||||
TCP = 'TCP',
|
||||
UDP = 'UDP',
|
||||
/** 可选 */
|
||||
ICMP = 'ICMP',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
/**
|
||||
* 进出站类型枚举
|
||||
*/
|
||||
export enum TrafficDirection {
|
||||
/** 入站 */
|
||||
Inbound = 'Inbound',
|
||||
/** 出站 */
|
||||
Outbound = 'Outbound',
|
||||
// Unknown
|
||||
}
|
||||
|
||||
@@ -64,18 +64,21 @@ const router = createRouter({
|
||||
path: '/monitor',
|
||||
name: 'monitor',
|
||||
component: MonitorView,
|
||||
meta: { layout: 'main' },
|
||||
},
|
||||
// 防火墙
|
||||
{
|
||||
path: '/firewall',
|
||||
name: 'firewall',
|
||||
component: FirewallView,
|
||||
meta: { layout: 'main' },
|
||||
},
|
||||
// 网络分析
|
||||
{
|
||||
path: '/analyse',
|
||||
name: 'analyse',
|
||||
component: AnalyseView,
|
||||
meta: { layout: 'main' },
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ export const useApplicationStore = defineStore('applicationInfoSub', () => {
|
||||
|
||||
const selectedAppIndex = computed(() => {
|
||||
if (!selectedApp.value) return -1
|
||||
return appInfos.value.findIndex((app) => app.Id === selectedApp.value?.Id)
|
||||
return appInfos.value.findIndex((app) => app.id === selectedApp.value?.id)
|
||||
})
|
||||
|
||||
const totalMemoryUsage = computed(() => {
|
||||
@@ -36,7 +36,7 @@ export const useApplicationStore = defineStore('applicationInfoSub', () => {
|
||||
appInfos.value = apps
|
||||
|
||||
// 如果当前选中的应用不在新列表中,清空选中
|
||||
if (selectedApp.value && !apps.find((app) => app.Id === selectedApp.value?.Id)) {
|
||||
if (selectedApp.value && !apps.find((app) => app.id === selectedApp.value?.id)) {
|
||||
selectedApp.value = null
|
||||
}
|
||||
}
|
||||
@@ -76,24 +76,24 @@ export const useApplicationStore = defineStore('applicationInfoSub', () => {
|
||||
}
|
||||
|
||||
const updateApp = (updatedApp: ApplicationType) => {
|
||||
const index = appInfos.value.findIndex((app) => app.Id === updatedApp.Id)
|
||||
const index = appInfos.value.findIndex((app) => app.id === updatedApp.id)
|
||||
if (index !== -1) {
|
||||
appInfos.value[index] = updatedApp
|
||||
|
||||
// 如果更新的是当前选中的应用,同步更新选中状态
|
||||
if (selectedApp.value?.Id === updatedApp.Id) {
|
||||
if (selectedApp.value?.id === updatedApp.id) {
|
||||
selectedApp.value = updatedApp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeApp = (appId: string) => {
|
||||
const index = appInfos.value.findIndex((app) => app.Id === appId)
|
||||
const index = appInfos.value.findIndex((app) => app.id === appId)
|
||||
if (index !== -1) {
|
||||
appInfos.value.splice(index, 1)
|
||||
|
||||
// 如果删除的是当前选中的应用,选择下一个
|
||||
if (selectedApp.value?.Id === appId) {
|
||||
if (selectedApp.value?.id === appId) {
|
||||
if (appInfos.value.length > 0) {
|
||||
// 优先选择同位置的应用,如果没有则选择前一个
|
||||
const newIndex = Math.min(index, appInfos.value.length - 1)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { ref, Ref, watch } from 'vue'
|
||||
import { ProcessType } from '@/types/process'
|
||||
import { useWebSocketStore } from '@/stores/websocketStore'
|
||||
import { WebSocketResponse } from '@/types/websocket'
|
||||
|
||||
export const useProcessStore = defineStore('processInfoSub', () => {
|
||||
// 进程信息
|
||||
const processInfos: Ref<ProcessType[]> = ref([])
|
||||
|
||||
function clear() {
|
||||
processInfos.value = []
|
||||
|
||||
}
|
||||
|
||||
function subscribe() {
|
||||
const webSocketStore = useWebSocketStore()
|
||||
const { isOpen } = storeToRefs(webSocketStore)
|
||||
watch(
|
||||
isOpen,
|
||||
(oldValue, newValue) => {
|
||||
if (oldValue || newValue) {
|
||||
webSocketStore.registerHandler('ProcessInfo', (data: WebSocketResponse<string>) => {
|
||||
// console.log(data.data)
|
||||
const parse: ProcessType[] = JSON.parse(data.data)
|
||||
console.log(parse);
|
||||
processInfos.value = parse
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,processInfos,clear
|
||||
}
|
||||
})
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { ProtocolType, TrafficDirection, ConnectionState } from '@/constants/enums'
|
||||
|
||||
/**
|
||||
* 进程信息
|
||||
*/
|
||||
export type ProcessType = {
|
||||
/**
|
||||
* 进程名称
|
||||
*/
|
||||
processName: string
|
||||
|
||||
/**
|
||||
* 进程ID
|
||||
*/
|
||||
processId: number
|
||||
|
||||
/**
|
||||
* 启动时间
|
||||
*/
|
||||
startTime: Date
|
||||
|
||||
/**
|
||||
* 是否退出
|
||||
*/
|
||||
hasExited: boolean
|
||||
|
||||
/**
|
||||
* 退出时间
|
||||
*/
|
||||
exitTime?: Date
|
||||
|
||||
/**
|
||||
* 退出代码
|
||||
*/
|
||||
exitCode?: number
|
||||
|
||||
/**
|
||||
* 占用内存
|
||||
*/
|
||||
useMemory: number
|
||||
|
||||
/**
|
||||
* 线程数
|
||||
*/
|
||||
threadCount: number
|
||||
|
||||
/**
|
||||
* 主模块路径
|
||||
*/
|
||||
mainModulePath?: string
|
||||
|
||||
/**
|
||||
* 启动文件名
|
||||
*/
|
||||
mainModuleName?: string
|
||||
|
||||
/**
|
||||
* 上行总量
|
||||
*/
|
||||
totalUploaded: number
|
||||
/**
|
||||
* 下行总量
|
||||
*/
|
||||
totalDownloaded: number
|
||||
/**
|
||||
* 上行速度
|
||||
*/
|
||||
uploadSpeed: number
|
||||
|
||||
/**
|
||||
* 下行速度
|
||||
*/
|
||||
downloadSpeed: number
|
||||
|
||||
/**
|
||||
* 所有连接
|
||||
*/
|
||||
connections: ConnectionInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* IP终结点接口
|
||||
*/
|
||||
export interface IPEndPoint {
|
||||
/** IP地址 */
|
||||
address: string
|
||||
/** 端口号 */
|
||||
port: number
|
||||
|
||||
addressFamily: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接详细信息接口
|
||||
*/
|
||||
export interface ConnectionInfo {
|
||||
/** 连接唯一标识键 */
|
||||
connectionKey: string
|
||||
/** 进程ID */
|
||||
processId: number
|
||||
/** 协议类型 */
|
||||
protocol: ProtocolType
|
||||
/** 本地终结点 */
|
||||
localEndpoint: IPEndPoint
|
||||
/** 目标终结点 */
|
||||
remoteEndpoint: IPEndPoint
|
||||
/** 连接状态 */
|
||||
state: ConnectionState
|
||||
/** 流量方向 */
|
||||
direction: TrafficDirection
|
||||
/** 连接开始时间 */
|
||||
startTime: Date
|
||||
/** 最后活跃时间 */
|
||||
lastActiveTime: Date
|
||||
/** 已发送字节数 */
|
||||
bytesSent: number
|
||||
/** 已接收字节数 */
|
||||
bytesReceived: number
|
||||
/** 当前发送速度(字节/秒) */
|
||||
currentSendSpeed: number
|
||||
/** 当前接收速度(字节/秒) */
|
||||
currentReceiveSpeed: number
|
||||
/** 是否活跃 */
|
||||
isActive: boolean
|
||||
}
|
||||
+8
-2
@@ -11,8 +11,14 @@ interface ResponseModel {
|
||||
* 订阅信息
|
||||
*/
|
||||
interface SubscriptionInfo {
|
||||
subscriptionType: 'ApplicationInfo'
|
||||
// ApplicationInfo ProcessInfo
|
||||
subscriptionType: string
|
||||
interval: number
|
||||
}
|
||||
|
||||
export { ResponseModel, SubscriptionInfo }
|
||||
interface SubscriptionProcessInfo extends SubscriptionInfo {
|
||||
// 订阅的进程列表
|
||||
processIds: number[]
|
||||
}
|
||||
|
||||
export { ResponseModel, SubscriptionInfo,SubscriptionProcessInfo }
|
||||
@@ -1,14 +1,14 @@
|
||||
// types/websocket.ts
|
||||
|
||||
// 基础消息格式
|
||||
export interface WebSocketMessage<T = any> {
|
||||
export interface WebSocketMessage<T = unknown> {
|
||||
command: string
|
||||
data?: T
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
// 响应消息格式
|
||||
export interface WebSocketResponse<T = any> {
|
||||
export interface WebSocketResponse<T = unknown> {
|
||||
type: string
|
||||
data?: T
|
||||
success?: boolean
|
||||
@@ -81,7 +81,7 @@ export interface WebSocketConfig {
|
||||
export interface SubscriptionRequest {
|
||||
type: string
|
||||
interval?: number
|
||||
params?: Record<string, any>
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
// 命令处理器类型
|
||||
export type CommandHandler = (data: WebSocketResponse) => void
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="header-content">
|
||||
<div class="header-info">
|
||||
<h2 class="main-header-title">网络连接监控</h2>
|
||||
<p class="main-header-subtitle">实时监控系统网络活动</p>
|
||||
<p class="main-header-subtitle">系统网络活动监控</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
@@ -15,7 +15,6 @@
|
||||
@update:value="$emit('update:searchQuery', $event)"
|
||||
placeholder="搜索连接、域名或IP..."
|
||||
size="medium"
|
||||
round
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon :component="SearchOutline" />
|
||||
@@ -32,12 +31,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 连接列表 -->
|
||||
<ConnectionsTable
|
||||
:connections="filteredConnections"
|
||||
:isMonitoring="isMonitoring"
|
||||
@select="$emit('selectConnection', $event)"
|
||||
@toggleMonitoring="isMonitoring = !isMonitoring"
|
||||
/>
|
||||
<connections-table1 :connections="filteredConnections" />
|
||||
|
||||
<!-- 时间轴分割线 -->
|
||||
<div class="resize-handle-horizontal" @mousedown="$emit('resizeTimeline', $event)">
|
||||
@@ -56,26 +50,102 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { NButton, NInput, NIcon } from 'naive-ui'
|
||||
import { SearchOutline } from '@vicons/ionicons5'
|
||||
import ConnectionsTable from './components/ConnectionsTable.vue'
|
||||
import EventTimeline from './components/EventTimeline.vue'
|
||||
import { FlashOutline } from '@vicons/ionicons5'
|
||||
import { useApplicationStore } from '@/stores/application'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ResponseModel, SubscriptionInfo, SubscriptionProcessInfo } from '@/types/response'
|
||||
import { httpClient } from '@/utils/http'
|
||||
import { useWebSocketStore } from '@/stores/websocketStore'
|
||||
import { useProcessStore } from '@/stores/processInfo'
|
||||
import { ApplicationType } from '@/types/infoModel'
|
||||
import ConnectionsTable1 from '@/views/MonitorView/components/ConnectionsTable1.vue'
|
||||
|
||||
const webSocketStore = useWebSocketStore()
|
||||
const { isOpen } = storeToRefs(webSocketStore)
|
||||
|
||||
const applicationStore = useApplicationStore()
|
||||
const { selectedApp } = storeToRefs(applicationStore)
|
||||
|
||||
const processStore = useProcessStore()
|
||||
const { processInfos } = storeToRefs(processStore)
|
||||
processStore.subscribe()
|
||||
|
||||
// 在父组件中
|
||||
const filteredConnections = computed(() => {
|
||||
return processInfos.value || []
|
||||
})
|
||||
|
||||
watch(selectedApp, (newVal, oldVal) => {
|
||||
// 进行更新,先把原始数据清空
|
||||
processStore.clear()
|
||||
|
||||
console.log(newVal.id, '==========')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 监听 WebSocket 状态
|
||||
watch(
|
||||
[isOpen, selectedApp],
|
||||
([newValue, newValue1]) => {
|
||||
if (newValue && newValue1 && newValue1.processIds && newValue1.processIds.length > 0) {
|
||||
// ApplicationType
|
||||
// 发送请求【请求订阅软件列表】
|
||||
const subAppInfo: SubscriptionProcessInfo = {
|
||||
subscriptionType: 'ProcessInfo',
|
||||
interval: 1000,
|
||||
processIds: newValue1.processIds,
|
||||
}
|
||||
|
||||
const data = JSON.stringify(subAppInfo)
|
||||
httpClient
|
||||
.post(`/subscribe-process`, data)
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('订阅进程信息成功:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('订阅进程信息失败:', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const subAppInfo: SubscriptionInfo = {
|
||||
subscriptionType: 'ProcessInfo',
|
||||
interval: 1,
|
||||
}
|
||||
|
||||
httpClient
|
||||
.post(`/unsubscribe`, JSON.stringify(subAppInfo))
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('取消订阅进程信息成功:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('取消订阅进程信息失败:', err)
|
||||
})
|
||||
})
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
headerHeight: number
|
||||
timelineHeight: number
|
||||
searchQuery: string
|
||||
connections: Array<any>
|
||||
events: Array<any>
|
||||
connections: Array<unknown>
|
||||
events: Array<unknown>
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:searchQuery': [value: string]
|
||||
'resizeTimeline': [event: MouseEvent]
|
||||
'selectConnection': [connection: any]
|
||||
resizeTimeline: [event: MouseEvent]
|
||||
selectConnection: [connection: unknown]
|
||||
}>()
|
||||
|
||||
// Local state
|
||||
@@ -83,16 +153,17 @@ const isMonitoring = ref(true)
|
||||
const isTimelinePaused = ref(false)
|
||||
|
||||
// 过滤后的连接列表
|
||||
const filteredConnections = computed(() => {
|
||||
if (!props.searchQuery) return props.connections
|
||||
|
||||
const query = props.searchQuery.toLowerCase()
|
||||
return props.connections.filter(conn =>
|
||||
conn.process.toLowerCase().includes(query) ||
|
||||
conn.localAddress.toLowerCase().includes(query) ||
|
||||
conn.remoteAddress.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
// const filteredConnections = computed(() => {
|
||||
// if (!props.searchQuery) return props.connections
|
||||
//
|
||||
// const query = props.searchQuery.toLowerCase()
|
||||
// return props.connections.filter(
|
||||
// (conn) =>
|
||||
// conn.process.toLowerCase().includes(query) ||
|
||||
// conn.localAddress.toLowerCase().includes(query) ||
|
||||
// conn.remoteAddress.toLowerCase().includes(query),
|
||||
// )
|
||||
// })
|
||||
|
||||
// 处理过滤
|
||||
const handleFilter = () => {
|
||||
|
||||
@@ -138,7 +138,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
httpClient
|
||||
.post(`/subscribe`, JSON.stringify(subAppInfo))
|
||||
.post(`/subscribe-application`, JSON.stringify(subAppInfo))
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('订阅应用信息成功:', res)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ watch(input, (value) => {
|
||||
:text="text"
|
||||
:patterns="patterns"
|
||||
:highlight-style="{
|
||||
padding: '0 6px',
|
||||
padding: '0 1px',
|
||||
borderRadius: themeVars.borderRadius,
|
||||
display: 'inline-block',
|
||||
color: themeVars.baseColor,
|
||||
|
||||
Reference in new issue
Block a user