A:监视搜索过滤

This commit is contained in:
jimmy committed 2025-07-27 14:49:20 +08:00
1 parent 09724afa90
commit 797fd99f4c
4 files changed
+323 -62

No files matched your search

@@ -12,13 +12,13 @@ export const useApplicationStore = defineStore('applicationInfoSub', () => {
// 计算属性
const activeApps = computed(() => {
return appInfos.value.filter((app) => !app.ExitCode)
return appInfos.value.filter((app) => !app.exitCode)
})
const isInspecting = computed(() => !!inspectingAppDetails.value)
const inactiveApps = computed(() => {
return appInfos.value.filter((app) => !!app.ExitCode)
return appInfos.value.filter((app) => !!app.exitCode)
})
const selectedAppIndex = computed(() => {
@@ -27,11 +27,11 @@ export const useApplicationStore = defineStore('applicationInfoSub', () => {
})
const totalMemoryUsage = computed(() => {
return appInfos.value.reduce((total, app) => total + app.UseMemory, 0)
return appInfos.value.reduce((total, app) => total + app.useMemory, 0)
})
const totalProcessCount = computed(() => {
return appInfos.value.reduce((total, app) => total + app.ProcessIds.length, 0)
return appInfos.value.reduce((total, app) => total + app.processIds.length, 0)
})
// 操作
@@ -0,0 +1,161 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useApplicationStore } from './application'
import { useProcessStore } from './processInfo'
import { ApplicationType } from '@/types/infoModel'
import { ProcessType } from '@/types/process'
export const useFilterStore = defineStore('filter', () => {
// 过滤文本
const filterText = ref('')
// 是否启用过滤
const isFiltering = ref(false)
// 获取stores
const applicationStore = useApplicationStore()
const processStore = useProcessStore()
// 检查文本是否匹配过滤条件
const isTextMatch = (text: string): boolean => {
if (!filterText.value || !isFiltering.value || !text) return false
return text.toLowerCase().includes(filterText.value.toLowerCase())
}
// 检查应用是否匹配过滤条件
const isApplicationMatch = (app: ApplicationType): boolean => {
if (!filterText.value || !isFiltering.value || !app) return false
const query = filterText.value.toLowerCase()
// 检查软件名称
if (app.productName && typeof app.productName === 'string' && app.productName.toLowerCase().includes(query)) return true
// 检查进程名称
if (app.processName && typeof app.processName === 'string' && app.processName.toLowerCase().includes(query)) return true
// 检查公司名称
if (app.companyName && typeof app.companyName === 'string' && app.companyName.toLowerCase().includes(query)) return true
// 检查进程ID
if (app.processIds && Array.isArray(app.processIds) && app.processIds.some(id => id && id.toString().includes(query))) return true
return false
}
// 检查进程是否匹配过滤条件
const isProcessMatch = (process: ProcessType): boolean => {
if (!filterText.value || !isFiltering.value || !process) return false
const query = filterText.value.toLowerCase()
// 检查进程名称
if (process.processName && typeof process.processName === 'string' && process.processName.toLowerCase().includes(query)) return true
// 检查进程ID
if (process.processId && process.processId.toString().includes(query)) return true
// 检查连接信息
if (process.connections && Array.isArray(process.connections) && process.connections.some(conn => {
if (!conn) return false
// 检查本地IP和端口
if ((conn.localEndpoint?.address && typeof conn.localEndpoint.address === 'string' && conn.localEndpoint.address.includes(query)) ||
(conn.localEndpoint?.port && conn.localEndpoint.port.toString().includes(query))) return true
// 检查远程IP和端口
if ((conn.remoteEndpoint?.address && typeof conn.remoteEndpoint.address === 'string' && conn.remoteEndpoint.address.includes(query)) ||
(conn.remoteEndpoint?.port && conn.remoteEndpoint.port.toString().includes(query))) return true
return false
})) return true
return false
}
// 过滤后的应用列表 - 真正的过滤,只显示匹配的项目
const filteredApplications = computed(() => {
// 如果没有启用过滤,显示所有应用
if (!isFiltering.value || !filterText.value.trim()) {
return applicationStore.appInfos
}
try {
// 只返回匹配的应用
return applicationStore.appInfos.filter(app => {
if (!app) return false
return isApplicationMatch(app)
})
} catch (error) {
console.error('过滤应用时出错:', error)
return applicationStore.appInfos
}
})
// 过滤后的进程列表 - 真正的过滤,只显示匹配的项目
const filteredProcesses = computed(() => {
// 如果没有启用过滤,显示所有进程
if (!isFiltering.value || !filterText.value.trim()) {
return processStore.processInfos
}
try {
// 只返回匹配的进程
return processStore.processInfos.filter(process => {
if (!process) return false
return isProcessMatch(process)
})
} catch (error) {
console.error('过滤进程时出错:', error)
return processStore.processInfos
}
})
// 设置过滤文本
const setFilterText = (text: string) => {
filterText.value = text
}
// 启用/禁用过滤
const setFiltering = (enabled: boolean) => {
isFiltering.value = enabled
}
// 切换过滤状态
const toggleFiltering = () => {
isFiltering.value = !isFiltering.value
}
// 清除过滤
const clearFilter = () => {
filterText.value = ''
isFiltering.value = false
}
// 应用过滤(激活过滤功能)
const applyFilter = () => {
if (filterText.value.trim()) {
isFiltering.value = true
}
}
return {
// 状态
filterText,
isFiltering,
// 计算属性
filteredApplications,
filteredProcesses,
// 方法
isTextMatch,
isApplicationMatch,
isProcessMatch,
setFilterText,
setFiltering,
toggleFiltering,
clearFilter,
applyFilter,
}
})
@@ -4,16 +4,28 @@
<div class="main-header" :style="{ height: headerHeight + 'px' }">
<div class="header-content">
<div class="header-info">
<h2 class="main-header-title">网络连接监控</h2>
<p class="main-header-subtitle">系统网络活动监控</p>
<h2 class="main-header-title">
{{ isFiltering && filterText ? '过滤结果 - 网络连接监控' : '网络连接监控' }}
</h2>
<p class="main-header-subtitle">
{{
isFiltering && filterText
? `正在显示包含 "${filterText}" 的连接信息`
: '系统网络活动监控'
}}
</p>
</div>
<div class="header-actions">
<div class="search-area">
<n-input
v-model:value="searchQuery"
placeholder="搜索连接、域名或IP..."
v-model:value="filterText"
placeholder="搜索连接、域名、IP、端口、进程名..."
size="medium"
clearable
@keydown.enter="handleFilter"
@keydown.escape="handleClearFilter"
@clear="handleClearFilter"
>
<template #prefix>
<n-icon :component="SearchOutline" />
@@ -22,8 +34,12 @@
</div>
<div class="filter-buttons">
<n-button size="small" type="primary" quaternary @click="handleFilter">
过滤
<n-button
size="small"
:type="isFiltering ? 'primary' : 'default'"
@click="handleFilter"
>
{{ isFiltering ? '取消过滤' : '过滤' }}
</n-button>
<n-button size="small" quaternary @click="handleRefresh">
刷新
@@ -35,8 +51,10 @@
<!-- 连接列表 -->
<connections-table1
:connections="filteredConnections"
:connections="filteredProcesses"
:height="connectionsTableHeight"
:filter-text="filterText"
:is-filtering="isFiltering"
/>
<!-- 时间轴分割线 -->
@@ -60,6 +78,7 @@ import { ResponseModel, SubscriptionInfo, SubscriptionProcessInfo } from '@/type
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'
@@ -72,6 +91,9 @@ const { selectedApp } = storeToRefs(applicationStore)
const processStore = useProcessStore()
const { processInfos } = storeToRefs(processStore)
const filterStore = useFilterStore()
const { filterText, isFiltering, filteredProcesses } = storeToRefs(filterStore)
// 订阅进程信息
@@ -106,7 +128,6 @@ const timelineResizing = ref<{
})
// 内部管理的状态
const searchQuery = ref('')
const isTimelinePaused = ref(false)
const expandedProcesses = ref<number[]>([])
@@ -116,31 +137,6 @@ const connectionsTableHeight = computed(() => {
const usedHeight = headerHeight.value + timelineHeight.value + 4 // 4px为分割线高度
return `${Math.max(200, totalHeight - usedHeight)}px`
})
// 过滤后的连接数据
const filteredConnections = computed(() => {
const connections = processInfos.value || []
// 如果有搜索条件,进行过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
return connections.filter(process => {
// 搜索进程名
if (process.processName.toLowerCase().includes(query)) {
return true
}
// 搜索连接信息
return process.connections.some(conn =>
conn.localEndpoint.address.includes(query) ||
conn.remoteEndpoint.address.includes(query) ||
conn.localEndpoint.port.toString().includes(query) ||
conn.remoteEndpoint.port.toString().includes(query)
)
})
}
return connections
})
// Timeline拖拽逻辑
const startTimelineResize = (event: MouseEvent) => {
event.preventDefault()
@@ -187,7 +183,18 @@ const updateMainViewHeight = () => {
// 事件处理函数
const handleFilter = () => {
console.log('打开过滤器')
if (isFiltering.value) {
// 当前正在过滤,点击取消过滤
filterStore.clearFilter()
} else {
// 当前没有过滤,点击应用过滤
filterStore.applyFilter()
}
}
// 清除过滤
const handleClearFilter = () => {
filterStore.clearFilter()
}
const handleRefresh = () => {
@@ -200,7 +207,7 @@ watch(selectedApp, (newVal, oldVal) => {
// 进行更新,先把原始数据清空
processStore.clear()
// 清空搜索
searchQuery.value = ''
filterStore.clearFilter()
console.log(newVal.id, '==========')
})
@@ -13,7 +13,13 @@
</template>
<div class="sidebar-header">
<h3 class="sidebar-title">所有连接</h3>
<h3 class="sidebar-title">
{{ isFiltering && filterText ? '过滤结果' : '所有连接' }}
</h3>
<div v-if="isFiltering && filterText" class="filter-indicator">
<span>搜索: "{{ filterText }}"</span>
<span class="filter-count">找到 {{ filteredApplications.length }} 个应用</span>
</div>
</div>
<!-- 应用列表容器 - 添加独立滚动 -->
@@ -26,7 +32,7 @@
@blur="handleListBlur"
>
<div
v-for="(app, index) in appInfos"
v-for="(app, index) in filteredApplications"
:key="app.id"
:ref="(el) => setAppItemRef(el, index)"
class="app-item"
@@ -57,10 +63,34 @@
<!-- 应用信息 -->
<div class="app-info">
<div class="app-name">{{ app.productName }}</div>
<div class="app-name">
<!-- 在过滤模式下不需要高亮,因为显示的都是匹配项 -->
<template v-if="isFiltering && filterText">
{{ getProductName(app) }}
</template>
<template v-else>
<n-highlight
:text="getProductName(app)"
:patterns="highlightPatterns"
:highlight-style="highlightStyle"
/>
</template>
</div>
<div class="app-details">
<span class="app-detail">线程数: {{ app.processIds.length }}</span>
<span class="app-detail">内存: {{ formatMemory(app.useMemory) }}</span>
<span class="app-detail">
线程数:
<template v-if="isFiltering && filterText">
{{ getProcessCount(app) }}
</template>
<template v-else>
<n-highlight
:text="getProcessCount(app)"
:patterns="highlightPatterns"
:highlight-style="highlightStyle"
/>
</template>
</span>
<span class="app-detail">内存: {{ formatMemory(app.useMemory || 0) }}</span>
<span class="app-detail">{{ !!app.exitCode ? '已退出' : '活动' }}</span>
</div>
</div>
@@ -75,10 +105,18 @@
</div>
<!-- 空状态 -->
<div v-if="appInfos.length === 0" class="empty-state">
<div v-if="filteredApplications.length === 0" class="empty-state">
<n-icon :component="DesktopOutline" size="48" class="empty-icon" />
<div class="empty-title">暂无运行的应用</div>
<div class="empty-subtitle">系统中没有检测到正在运行的应用程序</div>
<div class="empty-title">
{{ isFiltering && filterText ? '没有找到匹配的应用' : '暂无运行的应用' }}
</div>
<div class="empty-subtitle">
{{
isFiltering && filterText
? `没有找到包含 "${filterText}" 的应用程序,请尝试其他搜索词`
: '系统中没有检测到正在运行的应用程序'
}}
</div>
</div>
</div>
</div>
@@ -87,14 +125,15 @@
</template>
<script setup lang="ts">
import { ref, defineProps, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { NIcon } from 'naive-ui'
import { ref, defineProps, watch, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { NIcon, NHighlight, useThemeVars } from 'naive-ui'
import { storeToRefs } from 'pinia'
import { DesktopOutline } from '@vicons/ionicons5'
import { httpClient } from '@/utils/http.ts'
import { ResponseModel, SubscriptionInfo } from '@/types/response'
import { useWebSocketStore } from '@/stores/websocketStore'
import { useApplicationStore } from '@/stores/application'
import { useFilterStore } from '@/stores/filterStore'
import { ApplicationType } from '@/types/infoModel'
import { convertFileSize } from '@/utils/fileUtil'
import { FILE_SIZE_UNIT_ENUM } from '@/constants/enums'
@@ -109,8 +148,39 @@ const props = defineProps<{
// Store
const webSocketStore = useWebSocketStore()
const applicationStore = useApplicationStore()
const filterStore = useFilterStore()
const { isOpen } = storeToRefs(webSocketStore)
const { appInfos, selectedApp } = storeToRefs(applicationStore)
const { selectedApp } = storeToRefs(applicationStore)
const { filterText, isFiltering, filteredApplications } = storeToRefs(filterStore)
// 主题变量
const themeVars = useThemeVars()
// 高亮模式
const highlightPatterns = computed(() => {
if (!isFiltering.value || !filterText.value) return []
return [filterText.value]
})
// 高亮样式
const highlightStyle = computed(() => ({
padding: '1px 2px',
borderRadius: themeVars.value.borderRadius,
display: 'inline-block',
color: themeVars.value.baseColor,
background: themeVars.value.primaryColor,
transition: `all .3s ${themeVars.value.cubicBezierEaseInOut}`,
}))
// 安全地获取产品名称
const getProductName = (app: ApplicationType) => {
return app?.productName || ''
}
// 安全地获取进程数量
const getProcessCount = (app: ApplicationType) => {
return app?.processIds?.length?.toString() || '0'
}
// Refs
const appListRef = ref<HTMLDivElement>()
@@ -152,7 +222,7 @@ onMounted(() => {
// 监听应用列表变化,自动选中第一个
watch(
appInfos,
filteredApplications,
async (newAppInfos) => {
// 重置聚焦索引
// focusedIndex.value = -1
@@ -203,7 +273,7 @@ onUnmounted(() => {
// 处理键盘事件
const handleKeyDown = (event: KeyboardEvent) => {
if (appInfos.value.length === 0) return
if (filteredApplications.value.length === 0) return
let newFocusedIndex = focusedIndex.value
@@ -212,18 +282,18 @@ const handleKeyDown = (event: KeyboardEvent) => {
event.preventDefault()
// 如果当前没有聚焦,从选中项开始
if (focusedIndex.value === -1 && selectedApp.value) {
const selectedIndex = appInfos.value.findIndex(
const selectedIndex = filteredApplications.value.findIndex(
(app: ApplicationType) => app.id === selectedApp.value?.id,
)
newFocusedIndex = selectedIndex > 0 ? selectedIndex - 1 : appInfos.value.length - 1
newFocusedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredApplications.value.length - 1
}
// 如果没有聚焦也没有选中,从最后一个开始
else if (focusedIndex.value === -1) {
newFocusedIndex = appInfos.value.length - 1
newFocusedIndex = filteredApplications.value.length - 1
}
// 正常向上移动
else if (focusedIndex.value === 0) {
newFocusedIndex = appInfos.value.length - 1
newFocusedIndex = filteredApplications.value.length - 1
} else {
newFocusedIndex = focusedIndex.value - 1
}
@@ -233,15 +303,15 @@ const handleKeyDown = (event: KeyboardEvent) => {
event.preventDefault()
// 如果当前没有聚焦,从选中项开始
if (focusedIndex.value === -1 && selectedApp.value) {
const selectedIndex = appInfos.value.findIndex((app) => app.id === selectedApp.value?.id)
newFocusedIndex = selectedIndex < appInfos.value.length - 1 ? selectedIndex + 1 : 0
const selectedIndex = filteredApplications.value.findIndex((app) => app.id === selectedApp.value?.id)
newFocusedIndex = selectedIndex < filteredApplications.value.length - 1 ? selectedIndex + 1 : 0
}
// 如果没有聚焦也没有选中,从第一个开始
else if (focusedIndex.value === -1) {
newFocusedIndex = 0
}
// 正常向下移动
else if (focusedIndex.value >= appInfos.value.length - 1) {
else if (focusedIndex.value >= filteredApplications.value.length - 1) {
newFocusedIndex = 0
} else {
newFocusedIndex = focusedIndex.value + 1
@@ -251,8 +321,8 @@ const handleKeyDown = (event: KeyboardEvent) => {
case 'Enter':
event.preventDefault()
// 如果有聚焦的应用,选中它
if (focusedIndex.value >= 0 && focusedIndex.value < appInfos.value.length) {
selectApp(appInfos.value[focusedIndex.value])
if (focusedIndex.value >= 0 && focusedIndex.value < filteredApplications.value.length) {
selectApp(filteredApplications.value[focusedIndex.value])
}
break
@@ -320,7 +390,7 @@ const selectApp = (app: ApplicationType) => {
applicationStore.setSelectedApp(app)
// 选中后,如果是通过键盘选中的,保持聚焦在当前项
// 如果是通过鼠标点击的,会在 mouseLeave 时自动清除聚焦
const selectedIndex = appInfos.value.findIndex((a: ApplicationType) => a.id === app.id)
const selectedIndex = filteredApplications.value.findIndex((a: ApplicationType) => a.id === app.id)
if (focusedIndex.value === selectedIndex) {
// 通过键盘选中的,保持聚焦
} else {
@@ -369,6 +439,29 @@ const handleMouseLeave = () => {
justify-content: space-between;
margin-bottom: 20px;
flex-shrink: 0; /* 防止压缩 */
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.filter-indicator {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: var(--text-muted);
background: rgba(59, 130, 246, 0.1);
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.2);
width: 100%;
text-align: left;
}
.filter-count {
color: var(--accent-primary);
font-weight: 600;
font-size: 11px;
}
.sidebar-title {