A:将网络连接展示调整虚拟列表
This commit is contained in:
1 parent
c5fbb2021c
commit
1b7203f3d2
3 files changed
+1416
-127
No files matched your search
+2
@@ -19,6 +19,8 @@ declare module 'vue' {
|
||||
NHighlight: typeof import('naive-ui')['NHighlight']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TitleBar: typeof import('./src/components/TitleBar.vue')['default']
|
||||
|
||||
@@ -23,7 +23,25 @@ export enum FILE_SIZE_UNIT_ENUM {
|
||||
* 连接状态枚举
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
/** 已建立连接 */
|
||||
ESTABLISHED = 'ESTABLISHED',
|
||||
/** 正在监听 */
|
||||
LISTEN = 'LISTEN',
|
||||
/** 等待关闭 */
|
||||
TIME_WAIT = 'TIME_WAIT',
|
||||
/** 等待远程关闭 */
|
||||
CLOSE_WAIT = 'CLOSE_WAIT',
|
||||
/** 正在关闭 */
|
||||
CLOSING = 'CLOSING',
|
||||
/** 已关闭 */
|
||||
CLOSED = 'CLOSED',
|
||||
/** 正在尝试连接 */
|
||||
SYN_SENT = 'SYN_SENT',
|
||||
/** 连接请求已收到 */
|
||||
SYN_RECEIVED = 'SYN_RECEIVED',
|
||||
/** 最后的ACK等待 */
|
||||
LAST_ACK = 'LAST_ACK',
|
||||
/** 连接中 */
|
||||
Connecting = 'Connecting',
|
||||
/** 当前连接活跃(发送/接收中) */
|
||||
Connected = 'Connected',
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
{{ isFiltering && filterText ? '过滤结果 - 网络连接监控' : '网络连接监控' }}
|
||||
</h2>
|
||||
<p class="main-header-subtitle">
|
||||
{{
|
||||
isFiltering && filterText
|
||||
? `正在显示包含 "${filterText}" 的连接信息`
|
||||
: '系统网络活动监控'
|
||||
{{
|
||||
isFiltering && filterText
|
||||
? `正在显示包含 "${filterText}" 的连接信息`
|
||||
: '系统网络活动监控'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
@@ -34,8 +34,8 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-buttons">
|
||||
<n-button
|
||||
size="small"
|
||||
<n-button
|
||||
size="small"
|
||||
:type="isFiltering ? 'primary' : 'default'"
|
||||
@click="handleFilter"
|
||||
>
|
||||
@@ -49,29 +49,326 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接列表 -->
|
||||
<connections-table1
|
||||
:connections="filteredProcesses"
|
||||
:height="connectionsTableHeight"
|
||||
:filter-text="filterText"
|
||||
:is-filtering="isFiltering"
|
||||
/>
|
||||
<!-- 统计卡片区域 -->
|
||||
<div class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper process">
|
||||
<n-icon :component="DesktopOutline" :size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.activeProcesses }}</div>
|
||||
<div class="stat-label">活跃进程 / {{ stats.totalProcesses }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间轴分割线 -->
|
||||
<!-- <div class="resize-handle-horizontal" @mousedown="startTimelineResize">-->
|
||||
<!-- <div class="resize-handle-hover-h"></div>-->
|
||||
<!-- </div>-->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper connection">
|
||||
<n-icon :component="GitNetworkOutline" :size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.activeConnections }}</div>
|
||||
<div class="stat-label">活跃连接 / {{ stats.totalConnections }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间轴事件流 -->
|
||||
<!-- <EventTimeline :height="timelineHeight" />-->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper upload">
|
||||
<n-icon :component="ArrowUpOutline" :size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ formatSpeed(stats.totalUploadSpeed) }}</div>
|
||||
<div class="stat-label">总上传速度</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper download">
|
||||
<n-icon :component="ArrowDownOutline" :size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ formatSpeed(stats.totalDownloadSpeed) }}</div>
|
||||
<div class="stat-label">总下载速度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进程过滤器 -->
|
||||
<div class="process-filter">
|
||||
<div class="filter-header">
|
||||
<div class="filter-title-section">
|
||||
<h3 class="filter-title">进程过滤器</h3>
|
||||
<div class="filter-actions">
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
@click="toggleProcessDetails"
|
||||
:type="showProcessDetails ? 'primary' : 'default'"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon :component="showProcessDetails ? EyeOutline : SettingsOutline" />
|
||||
</template>
|
||||
{{ showProcessDetails ? '隐藏详情' : '显示详情' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
@click="toggleAllProcesses"
|
||||
>
|
||||
{{ selectedProcessIds.size === 0 ? '全选' : '清空' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-stats">
|
||||
<span class="stats-text">
|
||||
已选择 {{ selectedProcessIds.size }} / {{ filteredProcesses.length }} 个进程
|
||||
{{ selectedProcessIds.size > 0 ? `· ${displayConnections.length} 个连接` : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="process-grid">
|
||||
<div
|
||||
v-for="process in processSelectors"
|
||||
:key="process.processId"
|
||||
class="process-card"
|
||||
:class="{
|
||||
'is-selected': process.isSelected,
|
||||
'is-active': !process.hasExited,
|
||||
'is-exited': process.hasExited
|
||||
}"
|
||||
@click="toggleProcessSelection(process.processId)"
|
||||
>
|
||||
<div class="process-card-header">
|
||||
<div class="process-card-icon">
|
||||
<n-icon :component="DesktopOutline" size="20" />
|
||||
</div>
|
||||
<div class="process-card-status">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="process.hasExited ? 'exited' : 'active'"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="process.isSelected" class="process-selected-indicator">
|
||||
<!-- 移除对号图标 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="process-card-content">
|
||||
<div class="process-card-name" :title="process.processName">
|
||||
{{ process.processName }}
|
||||
</div>
|
||||
<div class="process-card-basic-info">
|
||||
<span class="basic-info-item">PID: {{ process.processId }}</span>
|
||||
<span class="basic-info-item connections">
|
||||
<n-icon :component="GitNetworkOutline" size="12" />
|
||||
{{ process.activeConnectionCount }}/{{ process.connectionCount }}
|
||||
</span>
|
||||
<span class="basic-info-item">
|
||||
<n-icon :component="TimeOutline" size="12" />
|
||||
{{ formatUptime(process.startTime) }}
|
||||
</span>
|
||||
<span class="basic-info-item">
|
||||
<n-icon :component="HardwareChipOutline" size="12" />
|
||||
{{ formatBytes(process.useMemory) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息(仅在选中且开启详情时显示) -->
|
||||
<div v-if="process.isSelected && showProcessDetails" class="process-card-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-item">
|
||||
<n-icon :component="GitBranchOutline" size="12" />
|
||||
{{ process.threadCount }} 线程
|
||||
</span>
|
||||
<span class="detail-item">
|
||||
总流量: {{ formatBytes(process.totalTraffic) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-row traffic">
|
||||
<span class="detail-item upload">
|
||||
<n-icon :component="ArrowUpOutline" size="12" />
|
||||
{{ formatSpeed(process.uploadSpeed || 0) }}
|
||||
</span>
|
||||
<span class="detail-item download">
|
||||
<n-icon :component="ArrowDownOutline" size="12" />
|
||||
{{ formatSpeed(process.downloadSpeed || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="process.mainModulePath" class="detail-row">
|
||||
<span class="detail-item path" :title="process.mainModulePath">
|
||||
路径: {{ process.mainModuleFileName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接列表容器 -->
|
||||
<div class="connections-container">
|
||||
<div class="connections-header">
|
||||
<h3 class="connections-title">
|
||||
网络连接
|
||||
<span class="connections-count">
|
||||
({{ displayConnections.length }} 个连接)
|
||||
</span>
|
||||
</h3>
|
||||
<div class="connections-actions">
|
||||
<n-select
|
||||
v-model:value="sortBy"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
:options="sortOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vue Virtual Scroller - 表格化连接列表 -->
|
||||
<div class="connections-table">
|
||||
<div class="table-header">
|
||||
<div class="table-cell header-cell status">状态</div>
|
||||
<div class="table-cell header-cell endpoints">连接信息</div>
|
||||
<div class="table-cell header-cell traffic">流量</div>
|
||||
<div class="table-cell header-cell time">时间</div>
|
||||
<div class="table-cell header-cell actions">操作</div>
|
||||
</div>
|
||||
|
||||
<RecycleScroller
|
||||
ref="virtualListRef"
|
||||
class="connections-scroller"
|
||||
:style="{ height: virtualListHeight }"
|
||||
:items="displayConnections"
|
||||
:item-size="60"
|
||||
key-field="key"
|
||||
v-slot="{ item }"
|
||||
>
|
||||
<div
|
||||
class="table-row connection-row"
|
||||
:class="{
|
||||
'is-active': item.isActive,
|
||||
'is-selected-process': selectedProcessIds.has(item.processId)
|
||||
}"
|
||||
>
|
||||
<div class="table-cell status-cell">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="item.isActive ? 'active' : 'inactive'"
|
||||
:title="item.isActive ? '活跃' : '空闲'"
|
||||
></div>
|
||||
<div class="status-info">
|
||||
<n-tag
|
||||
:type="getConnectionStateTagType(item.state, item.isActive)"
|
||||
size="tiny"
|
||||
round
|
||||
>
|
||||
{{ formatConnectionState(item.state) }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
:type="getProtocolTagType(item.protocol)"
|
||||
size="tiny"
|
||||
round
|
||||
style="margin-left: 4px;"
|
||||
>
|
||||
{{ formatProtocol(item.protocol) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-cell endpoints-cell">
|
||||
<div class="endpoint-row">
|
||||
<div class="endpoint local">
|
||||
<n-icon :component="LocationOutline" size="12" />
|
||||
<span class="endpoint-address">{{ formatEndpoint(item.localEndpoint) }}</span>
|
||||
</div>
|
||||
<n-icon :component="ArrowForwardOutline" size="12" class="connection-arrow" />
|
||||
<div class="endpoint remote">
|
||||
<n-icon :component="GlobeOutline" size="12" />
|
||||
<span class="endpoint-address">{{ formatEndpoint(item.remoteEndpoint) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-cell traffic-cell">
|
||||
<div class="traffic-row">
|
||||
<div class="traffic-item upload">
|
||||
<n-icon :component="ArrowUpOutline" size="12" />
|
||||
<span>{{ formatSpeed(item.currentSendSpeed || 0) }}</span>
|
||||
</div>
|
||||
<div class="traffic-item download">
|
||||
<n-icon :component="ArrowDownOutline" size="12" />
|
||||
<span>{{ formatSpeed(item.currentReceiveSpeed || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-cell time-cell">
|
||||
<div v-if="item.startTime" class="time-info">
|
||||
<span class="duration">{{ formatDuration(new Date(item.startTime)) }}</span>
|
||||
<span v-if="!item.isActive && item.lastActiveTime" class="last-active">
|
||||
{{ getTimeSinceActive(new Date(item.lastActiveTime)) }}前
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-cell actions-cell">
|
||||
<n-button size="tiny" circle quaternary type="info" @click.stop="handleViewDetails(item)">
|
||||
<template #icon>
|
||||
<n-icon :component="InformationCircleOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="tiny" circle quaternary type="warning" @click.stop="handleDisconnect(item)">
|
||||
<template #icon>
|
||||
<n-icon :component="StopCircleOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
|
||||
<!-- 无数据提示 -->
|
||||
<div v-if="displayConnections.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<n-icon :component="CloudOfflineOutline" size="64" />
|
||||
</div>
|
||||
<h3>暂无网络连接数据</h3>
|
||||
<p>{{ allConnections.length === 0 ? '系统中暂无活跃的网络连接' : '当前过滤条件下无匹配的连接' }}</p>
|
||||
<div v-if="selectedProcessIds.size > 0" class="empty-actions">
|
||||
<n-button size="small" @click="selectedProcessIds.clear(); selectedProcessIds = new Set()">
|
||||
清除进程过滤
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, onUnmounted, nextTick } from 'vue'
|
||||
import { NButton, NInput, NIcon } from 'naive-ui'
|
||||
import { SearchOutline } from '@vicons/ionicons5'
|
||||
import EventTimeline from './components/EventTimeline.vue'
|
||||
import { NButton, NInput, NIcon, NTag, NTooltip, NSelect } from 'naive-ui'
|
||||
import {
|
||||
SearchOutline,
|
||||
DesktopOutline,
|
||||
GitNetworkOutline,
|
||||
ArrowUpOutline,
|
||||
ArrowDownOutline,
|
||||
HardwareChipOutline,
|
||||
GitBranchOutline,
|
||||
LocationOutline,
|
||||
GlobeOutline,
|
||||
ArrowForwardOutline,
|
||||
TimeOutline,
|
||||
PauseCircleOutline,
|
||||
InformationCircleOutline,
|
||||
StopCircleOutline,
|
||||
CloudOfflineOutline,
|
||||
EyeOutline,
|
||||
SettingsOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import { useApplicationStore } from '@/stores/application'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ResponseModel, SubscriptionInfo, SubscriptionProcessInfo } from '@/types/response'
|
||||
@@ -79,8 +376,10 @@ import { httpClient } from '@/utils/http'
|
||||
import { useWebSocketStore } from '@/stores/websocketStore'
|
||||
import { useProcessStore } from '@/stores/processInfo'
|
||||
import { useFilterStore } from '@/stores/filterStore'
|
||||
|
||||
import ConnectionsTable1 from '@/views/MonitorView/components/ConnectionsTable1.vue'
|
||||
import type { ProcessType, ConnectionInfo, IPEndPoint } from '@/types/process'
|
||||
import { ConnectionState, FILE_SIZE_UNIT_ENUM } from '@/constants/enums'
|
||||
import { convertFileSize } from '@/utils/fileUtil'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
const webSocketStore = useWebSocketStore()
|
||||
const { isOpen } = storeToRefs(webSocketStore)
|
||||
@@ -94,105 +393,383 @@ const { processInfos } = storeToRefs(processStore)
|
||||
const filterStore = useFilterStore()
|
||||
const { filterText, isFiltering, filteredProcesses } = storeToRefs(filterStore)
|
||||
|
||||
|
||||
|
||||
// 订阅进程信息
|
||||
processStore.subscribe()
|
||||
|
||||
// Props - 只保留必要的布局相关属性
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
width?: number // 可选的宽度,如果父组件需要控制
|
||||
width?: number
|
||||
}>()
|
||||
|
||||
// 组件引用
|
||||
const mainViewRef = ref<HTMLElement>()
|
||||
const virtualListRef = ref()
|
||||
|
||||
// 内部管理的布局尺寸
|
||||
// 布局尺寸
|
||||
const headerHeight = ref(80)
|
||||
const timelineHeight = ref(200)
|
||||
const mainViewHeight = ref(600)
|
||||
|
||||
// Timeline拖拽限制
|
||||
const MIN_TIMELINE_HEIGHT = 100
|
||||
const MAX_TIMELINE_HEIGHT = 800
|
||||
// 选中的进程ID集合
|
||||
const selectedProcessIds = ref<Set<number>>(new Set())
|
||||
|
||||
// Timeline拖拽状态
|
||||
const timelineResizing = ref<{
|
||||
isResizing: boolean
|
||||
startY: number
|
||||
startHeight: number
|
||||
}>({
|
||||
isResizing: false,
|
||||
startY: 0,
|
||||
startHeight: 0,
|
||||
})
|
||||
// 进程详细信息显示控制
|
||||
const showProcessDetails = ref(false)
|
||||
|
||||
// 内部管理的状态
|
||||
const isTimelinePaused = ref(false)
|
||||
const expandedProcesses = ref<number[]>([])
|
||||
// 排序选项
|
||||
const sortBy = ref('time')
|
||||
const sortOptions = [
|
||||
{ label: '时间', value: 'time' },
|
||||
{ label: '流量', value: 'traffic' },
|
||||
{ label: '状态', value: 'state' },
|
||||
{ label: '进程', value: 'process' },
|
||||
]
|
||||
|
||||
// 计算ConnectionsTable1的可用高度
|
||||
const connectionsTableHeight = computed(() => {
|
||||
// 计算虚拟列表高度
|
||||
const virtualListHeight = computed(() => {
|
||||
const totalHeight = mainViewHeight.value
|
||||
const usedHeight = headerHeight.value + timelineHeight.value + 4 // 4px为分割线高度
|
||||
const usedHeight = headerHeight.value + 140 + 120 + 50 // header + stats + selector + connections header
|
||||
return `${Math.max(200, totalHeight - usedHeight)}px`
|
||||
})
|
||||
// Timeline拖拽逻辑
|
||||
const startTimelineResize = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
timelineResizing.value = {
|
||||
isResizing: true,
|
||||
startY: event.clientY,
|
||||
startHeight: timelineHeight.value,
|
||||
// 计算统计数据
|
||||
const stats = computed(() => {
|
||||
const activeProcesses = filteredProcesses.value.filter((p) => !p.hasExited)
|
||||
const totalConnections = filteredProcesses.value.reduce((sum, p) => sum + p.connections.length, 0)
|
||||
const activeConnections = filteredProcesses.value.reduce(
|
||||
(sum, p) => sum + p.connections.filter((c) => c.isActive).length,
|
||||
0,
|
||||
)
|
||||
const totalUploadSpeed = activeProcesses.reduce((sum, p) => sum + (p.uploadSpeed || 0), 0)
|
||||
const totalDownloadSpeed = activeProcesses.reduce((sum, p) => sum + (p.downloadSpeed || 0), 0)
|
||||
|
||||
return {
|
||||
activeProcesses: activeProcesses.length,
|
||||
totalProcesses: filteredProcesses.value.length,
|
||||
totalConnections,
|
||||
activeConnections,
|
||||
totalUploadSpeed,
|
||||
totalDownloadSpeed,
|
||||
}
|
||||
})
|
||||
|
||||
// 获取所有连接列表(统一显示)
|
||||
const allConnections = computed(() => {
|
||||
const connections: any[] = []
|
||||
|
||||
filteredProcesses.value.forEach((process) => {
|
||||
process.connections.forEach((connection, index) => {
|
||||
connections.push({
|
||||
key: `${process.processId}-${index}`,
|
||||
...connection,
|
||||
processId: process.processId,
|
||||
processName: process.processName,
|
||||
processInfo: process, // 添加完整的进程信息
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return connections
|
||||
})
|
||||
|
||||
// 根据选中的进程过滤连接
|
||||
const displayConnections = computed(() => {
|
||||
let connections = allConnections.value
|
||||
|
||||
// 如果选中了特定进程,只显示这些进程的连接
|
||||
if (selectedProcessIds.value.size > 0) {
|
||||
connections = connections.filter(conn => selectedProcessIds.value.has(conn.processId))
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleTimelineResize)
|
||||
document.addEventListener('mouseup', stopTimelineResize)
|
||||
document.body.style.cursor = 'row-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
// 根据排序选项排序
|
||||
return connections.sort((a, b) => {
|
||||
switch (sortBy.value) {
|
||||
case 'time':
|
||||
return new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
|
||||
case 'traffic':
|
||||
const aTraffic = (a.currentSendSpeed || 0) + (a.currentReceiveSpeed || 0)
|
||||
const bTraffic = (b.currentSendSpeed || 0) + (b.currentReceiveSpeed || 0)
|
||||
return bTraffic - aTraffic
|
||||
case 'state':
|
||||
const aState = String(a.state || '')
|
||||
const bState = String(b.state || '')
|
||||
return aState.localeCompare(bState)
|
||||
case 'process':
|
||||
return a.processName.localeCompare(b.processName)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 获取进程选择器显示的进程信息
|
||||
const processSelectors = computed(() => {
|
||||
return filteredProcesses.value.map(process => ({
|
||||
...process,
|
||||
isSelected: selectedProcessIds.value.has(process.processId),
|
||||
connectionCount: process.connections.length,
|
||||
activeConnectionCount: process.connections.filter(c => c.isActive).length,
|
||||
// 添加更多源信息
|
||||
totalTraffic: (process.totalUploaded || 0) + (process.totalDownloaded || 0),
|
||||
currentTraffic: (process.uploadSpeed || 0) + (process.downloadSpeed || 0),
|
||||
mainModuleFileName: process.mainModuleName || process.processName,
|
||||
uptimeMs: new Date().getTime() - new Date(process.startTime).getTime(),
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
// 切换进程选择
|
||||
const toggleProcessSelection = (processId: number) => {
|
||||
if (selectedProcessIds.value.has(processId)) {
|
||||
selectedProcessIds.value.delete(processId)
|
||||
} else {
|
||||
selectedProcessIds.value.add(processId)
|
||||
}
|
||||
selectedProcessIds.value = new Set(selectedProcessIds.value)
|
||||
}
|
||||
|
||||
const handleTimelineResize = (event: MouseEvent) => {
|
||||
if (!timelineResizing.value.isResizing) return
|
||||
|
||||
const { startY, startHeight } = timelineResizing.value
|
||||
const deltaY = startY - event.clientY // 向上拖拽为正值
|
||||
const newHeight = Math.min(
|
||||
MAX_TIMELINE_HEIGHT,
|
||||
Math.max(MIN_TIMELINE_HEIGHT, startHeight + deltaY)
|
||||
)
|
||||
|
||||
timelineHeight.value = newHeight
|
||||
// 切换进程详细信息显示
|
||||
const toggleProcessDetails = () => {
|
||||
showProcessDetails.value = !showProcessDetails.value
|
||||
}
|
||||
|
||||
const stopTimelineResize = () => {
|
||||
timelineResizing.value.isResizing = false
|
||||
document.removeEventListener('mousemove', handleTimelineResize)
|
||||
document.removeEventListener('mouseup', stopTimelineResize)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
// 格式化协议类型
|
||||
const formatProtocol = (protocol: any): string => {
|
||||
// 如果是数字,转换为对应的协议名称
|
||||
if (typeof protocol === 'number') {
|
||||
switch (protocol) {
|
||||
case 6:
|
||||
return 'TCP'
|
||||
case 17:
|
||||
return 'UDP'
|
||||
case 1:
|
||||
return 'ICMP'
|
||||
default:
|
||||
return `协议${protocol}`
|
||||
}
|
||||
}
|
||||
// 如果已经是字符串,直接返回
|
||||
return String(protocol || 'Unknown')
|
||||
}
|
||||
|
||||
// 更新主视图高度
|
||||
const updateMainViewHeight = () => {
|
||||
if (mainViewRef.value) {
|
||||
mainViewHeight.value = mainViewRef.value.clientHeight
|
||||
// 格式化连接状态
|
||||
const formatConnectionState = (state: any): string => {
|
||||
// 如果是数字,转换为对应的状态名称
|
||||
if (typeof state === 'number') {
|
||||
switch (state) {
|
||||
case 1:
|
||||
return '已建立' // ESTABLISHED
|
||||
case 2:
|
||||
return '正在监听' // LISTEN
|
||||
case 3:
|
||||
return '等待关闭' // TIME_WAIT
|
||||
case 4:
|
||||
return '等待远程关闭' // CLOSE_WAIT
|
||||
case 5:
|
||||
return '正在关闭' // CLOSING
|
||||
case 6:
|
||||
return '已关闭' // CLOSED
|
||||
case 7:
|
||||
return '正在连接' // SYN_SENT
|
||||
case 8:
|
||||
return '连接请求已收到' // SYN_RECEIVED
|
||||
case 9:
|
||||
return '最后ACK等待' // LAST_ACK
|
||||
default:
|
||||
return `状态${state}`
|
||||
}
|
||||
}
|
||||
// 如果已经是字符串,进行中文翻译
|
||||
const stateStr = String(state || 'Unknown')
|
||||
switch (stateStr) {
|
||||
case 'ESTABLISHED':
|
||||
return '已建立'
|
||||
case 'LISTEN':
|
||||
return '正在监听'
|
||||
case 'TIME_WAIT':
|
||||
return '等待关闭'
|
||||
case 'CLOSE_WAIT':
|
||||
return '等待远程关闭'
|
||||
case 'CLOSING':
|
||||
return '正在关闭'
|
||||
case 'CLOSED':
|
||||
return '已关闭'
|
||||
case 'SYN_SENT':
|
||||
return '正在连接'
|
||||
case 'SYN_RECEIVED':
|
||||
return '连接请求已收到'
|
||||
case 'LAST_ACK':
|
||||
return '最后ACK等待'
|
||||
case 'Connecting':
|
||||
return '连接中'
|
||||
case 'Connected':
|
||||
return '已连接'
|
||||
case 'Disconnected':
|
||||
return '已断开'
|
||||
case 'Listening':
|
||||
return '正在监听'
|
||||
default:
|
||||
return stateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化存活时间
|
||||
const formatUptime = (startTime: Date): string => {
|
||||
const diff = new Date().getTime() - new Date(startTime).getTime()
|
||||
const days = Math.floor(diff / 86400000)
|
||||
const hours = Math.floor((diff % 86400000) / 3600000)
|
||||
const minutes = Math.floor((diff % 3600000) / 60000)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天${hours}小时`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
} else {
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
}
|
||||
|
||||
// 全选/清空进程
|
||||
const toggleAllProcesses = () => {
|
||||
if (selectedProcessIds.value.size === 0) {
|
||||
filteredProcesses.value.forEach(p => selectedProcessIds.value.add(p.processId))
|
||||
} else {
|
||||
selectedProcessIds.value.clear()
|
||||
}
|
||||
selectedProcessIds.value = new Set(selectedProcessIds.value)
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
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))
|
||||
return value + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const formatSpeed = (bytesPerSecond: number): string => {
|
||||
const fileSize = convertFileSize(bytesPerSecond, FILE_SIZE_UNIT_ENUM.B)
|
||||
return fileSize.size + fileSize.unit + '/s'
|
||||
}
|
||||
|
||||
const formatEndpoint = (endpoint: IPEndPoint): string => {
|
||||
return `${endpoint.address}:${endpoint.port}`
|
||||
}
|
||||
|
||||
const formatDuration = (start: Date): string => {
|
||||
const diff = new Date().getTime() - start.getTime()
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const minutes = Math.floor((diff % 3600000) / 60000)
|
||||
const seconds = Math.floor((diff % 60000) / 1000)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}时${minutes}分`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分${seconds}秒`
|
||||
} else {
|
||||
return `${seconds}秒`
|
||||
}
|
||||
}
|
||||
|
||||
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 getProtocolTagType = (protocol: any) => {
|
||||
const protocolStr = formatProtocol(protocol)
|
||||
switch (protocolStr) {
|
||||
case 'TCP':
|
||||
return 'info' as const
|
||||
case 'UDP':
|
||||
return 'warning' as const
|
||||
case 'ICMP':
|
||||
return 'error' as const
|
||||
default:
|
||||
return 'default' as const
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接状态标签类型
|
||||
const getConnectionStateTagType = (state: any, isActive: boolean) => {
|
||||
if (!isActive) return 'default' as const
|
||||
|
||||
const stateStr = String(state || '')
|
||||
// 检查数字状态
|
||||
if (typeof state === 'number') {
|
||||
switch (state) {
|
||||
case 1: // ESTABLISHED
|
||||
return 'success' as const
|
||||
case 2: // LISTEN
|
||||
return 'info' as const
|
||||
case 3: // TIME_WAIT
|
||||
case 4: // CLOSE_WAIT
|
||||
return 'warning' as const
|
||||
case 5: // CLOSING
|
||||
case 6: // CLOSED
|
||||
return 'error' as const
|
||||
case 7: // SYN_SENT
|
||||
case 8: // SYN_RECEIVED
|
||||
return 'primary' as const
|
||||
default:
|
||||
return 'default' as const
|
||||
}
|
||||
}
|
||||
|
||||
// 检查字符串状态
|
||||
switch (stateStr) {
|
||||
case 'ESTABLISHED':
|
||||
return 'success' as const
|
||||
case 'LISTEN':
|
||||
case 'Listening':
|
||||
return 'info' as const
|
||||
case 'TIME_WAIT':
|
||||
case 'CLOSE_WAIT':
|
||||
return 'warning' as const
|
||||
case 'CLOSING':
|
||||
case 'CLOSED':
|
||||
case 'Disconnected':
|
||||
return 'error' as const
|
||||
case 'SYN_SENT':
|
||||
case 'SYN_RECEIVED':
|
||||
case 'Connecting':
|
||||
return 'primary' as const
|
||||
case 'Connected':
|
||||
return 'success' as const
|
||||
default:
|
||||
return 'default' as const
|
||||
}
|
||||
}
|
||||
|
||||
const getConnectionStateType = (state: ConnectionState, isActive: boolean) => {
|
||||
if (!isActive) return 'default' as const
|
||||
switch (state) {
|
||||
case ConnectionState.ESTABLISHED:
|
||||
return 'success' as const
|
||||
case ConnectionState.LISTEN:
|
||||
return 'info' as const
|
||||
case ConnectionState.TIME_WAIT:
|
||||
case ConnectionState.CLOSE_WAIT:
|
||||
return 'warning' as const
|
||||
default:
|
||||
return 'default' as const
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理函数
|
||||
const handleFilter = () => {
|
||||
if (isFiltering.value) {
|
||||
// 当前正在过滤,点击取消过滤
|
||||
filterStore.clearFilter()
|
||||
} else {
|
||||
// 当前没有过滤,点击应用过滤
|
||||
filterStore.applyFilter()
|
||||
}
|
||||
}
|
||||
|
||||
// 清除过滤
|
||||
const handleClearFilter = () => {
|
||||
filterStore.clearFilter()
|
||||
}
|
||||
@@ -202,31 +779,40 @@ const handleRefresh = () => {
|
||||
processStore.refresh?.()
|
||||
}
|
||||
|
||||
const handleViewDetails = (connection: ConnectionInfo) => {
|
||||
console.log('查看连接详情:', connection)
|
||||
}
|
||||
|
||||
const handleDisconnect = (connection: ConnectionInfo) => {
|
||||
console.log('断开连接:', connection)
|
||||
}
|
||||
|
||||
// 更新主视图高度
|
||||
const updateMainViewHeight = () => {
|
||||
if (mainViewRef.value) {
|
||||
mainViewHeight.value = mainViewRef.value.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选中应用的变化
|
||||
watch(selectedApp, (newVal, oldVal) => {
|
||||
// 进行更新,先把原始数据清空
|
||||
processStore.clear()
|
||||
// 清空搜索
|
||||
filterStore.clearFilter()
|
||||
|
||||
selectedProcessIds.value.clear()
|
||||
console.log(newVal.id, '==========')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化主视图高度
|
||||
nextTick(() => {
|
||||
updateMainViewHeight()
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', updateMainViewHeight)
|
||||
|
||||
// 监听 WebSocket 状态
|
||||
watch(
|
||||
[isOpen, selectedApp],
|
||||
([newValue, newValue1]) => {
|
||||
if (newValue && newValue1 && newValue1.processIds && newValue1.processIds.length > 0) {
|
||||
// 发送请求【请求订阅软件列表】
|
||||
const subAppInfo: SubscriptionProcessInfo = {
|
||||
subscriptionType: 'ProcessInfo',
|
||||
interval: 400,
|
||||
@@ -235,13 +821,13 @@ onMounted(() => {
|
||||
|
||||
const data = JSON.stringify(subAppInfo)
|
||||
httpClient
|
||||
.post(`/subscribe-process`, data)
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('订阅进程信息成功:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('订阅进程信息失败:', err)
|
||||
})
|
||||
.post(`/subscribe-process`, data)
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('订阅进程信息成功:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('订阅进程信息失败:', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -249,10 +835,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听
|
||||
window.removeEventListener('resize', updateMainViewHeight)
|
||||
document.removeEventListener('mousemove', handleTimelineResize)
|
||||
document.removeEventListener('mouseup', stopTimelineResize)
|
||||
|
||||
const subAppInfo: SubscriptionInfo = {
|
||||
subscriptionType: 'ProcessInfo',
|
||||
@@ -260,13 +843,13 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
httpClient
|
||||
.post(`/unsubscribe`, JSON.stringify(subAppInfo))
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('取消订阅进程信息成功:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('取消订阅进程信息失败:', err)
|
||||
})
|
||||
.post(`/unsubscribe`, JSON.stringify(subAppInfo))
|
||||
.then((res: ResponseModel) => {
|
||||
console.log('取消订阅进程信息成功:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('取消订阅进程信息失败:', err)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -278,7 +861,7 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--bg-card);
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -306,7 +889,7 @@ onUnmounted(() => {
|
||||
.main-header-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
@@ -335,35 +918,621 @@ onUnmounted(() => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 时间轴分割线 */
|
||||
.resize-handle-horizontal {
|
||||
height: 4px;
|
||||
background: var(--border-tertiary);
|
||||
cursor: row-resize;
|
||||
transition: background-color 0.2s;
|
||||
/* 统计卡片区域 */
|
||||
.stats-section {
|
||||
padding: 16px 24px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 进程过滤器 */
|
||||
.process-filter {
|
||||
padding: 16px 24px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.process-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.process-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.process-card:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.process-card.is-selected {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.process-card.is-exited {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.process-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.process-card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.process-card.is-selected .process-card-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.process-card-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.process-selected-indicator {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
color: #3b82f6;
|
||||
/* 隐藏选中指示器 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.process-card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.process-card-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.process-card-basic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.basic-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.basic-info-item.connections {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.process-card-details {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-row.traffic .detail-item.upload {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.detail-row.traffic .detail-item.download {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
|
||||
}
|
||||
|
||||
.status-indicator.exited {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
/* 连接列表容器 */
|
||||
.connections-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
/* 表格样式连接列表 */
|
||||
.connections-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 120px 100px 80px;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.connections-scroller {
|
||||
flex: 1;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 0 0 8px 8px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 120px 100px 80px;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
transition: all 0.2s ease;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.table-row.is-active {
|
||||
background: rgba(16, 185, 129, 0.03);
|
||||
}
|
||||
|
||||
.table-row.is-selected-process {
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-info .n-tag {
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.status-info .n-tag:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 协议标签自定义样式 */
|
||||
.status-info .n-tag--info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #2563eb;
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.status-info .n-tag--warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
border-color: rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.status-info .n-tag--error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.status-info .n-tag--success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #16a34a;
|
||||
border-color: rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
.status-info .n-tag--primary {
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
color: #9333ea;
|
||||
border-color: rgba(168, 85, 247, 0.2);
|
||||
}
|
||||
|
||||
.status-info .n-tag--default {
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
color: #6b7280;
|
||||
border-color: rgba(107, 114, 128, 0.2);
|
||||
}
|
||||
|
||||
.endpoints-cell {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint.local {
|
||||
color: var(--text-success);
|
||||
}
|
||||
|
||||
.endpoint.remote {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.endpoint-address {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connection-arrow {
|
||||
color: var(--text-quaternary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resize-handle-horizontal:hover {
|
||||
background: rgba(168, 85, 247, 0.5);
|
||||
.traffic-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.resize-handle-hover-h {
|
||||
position: absolute;
|
||||
inset: -4px 0;
|
||||
.traffic-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.resize-handle-horizontal:active {
|
||||
background-color: var(--accent-primary) !important;
|
||||
.traffic-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.traffic-item.upload {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.traffic-item.download {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.last-active {
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.connections-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.connections-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.connections-count {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.connections-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
color: var(--text-quaternary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* CSS 变量定义 */
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f9fafb;
|
||||
--bg-card: #ffffff;
|
||||
--bg-hover: #f3f4f6;
|
||||
--bg-glass: rgba(255, 255, 255, 0.8);
|
||||
--backdrop-blur: blur(8px);
|
||||
|
||||
--border-primary: #e5e7eb;
|
||||
--border-secondary: #f3f4f6;
|
||||
--border-hover: #d1d5db;
|
||||
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #4b5563;
|
||||
--text-muted: #6b7280;
|
||||
--text-quaternary: #9ca3af;
|
||||
--text-success: #059669;
|
||||
}
|
||||
|
||||
/* 暗色模式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #111827;
|
||||
--bg-card: #1f2937;
|
||||
--bg-hover: #374151;
|
||||
--bg-glass: rgba(31, 41, 55, 0.8);
|
||||
|
||||
--border-primary: #374151;
|
||||
--border-secondary: #1f2937;
|
||||
--border-hover: #4b5563;
|
||||
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--text-quaternary: #6b7280;
|
||||
--text-success: #10b981;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义 Naive UI 样式 */
|
||||
:deep(.n-input .n-input__input-el) {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-input:hover .n-input__input-el) {
|
||||
@@ -371,7 +1540,37 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
:deep(.n-input.n-input--focus .n-input__input-el) {
|
||||
border-color: var(--accent-primary);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
:deep(.n-button) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.n-tag) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.n-virtual-list) {
|
||||
--n-color: transparent;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
:deep(.n-virtual-list__content)::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
:deep(.n-virtual-list__content)::-webkit-scrollbar-track {
|
||||
background: var(--border-secondary);
|
||||
}
|
||||
|
||||
:deep(.n-virtual-list__content)::-webkit-scrollbar-thumb {
|
||||
background: var(--border-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.n-virtual-list__content)::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@@ -387,6 +1586,10 @@ onUnmounted(() => {
|
||||
.search-area {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -394,6 +1597,12 @@ onUnmounted(() => {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.stats-section,
|
||||
.process-filter,
|
||||
.connections-container {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
@@ -411,6 +1620,31 @@ onUnmounted(() => {
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.process-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.process-card {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
grid-template-columns: 100px 1fr 100px 80px 60px;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: 100px 1fr 100px 80px 60px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-info .n-tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@@ -436,6 +1670,30 @@ onUnmounted(() => {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.process-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -446,5 +1704,16 @@ onUnmounted(() => {
|
||||
.search-area {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.filter-title-section {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user