A:监视搜索过滤
This commit is contained in:
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 {
|
||||
|
||||
Reference in new issue
Block a user