feat(frontend): preserve page filters during navigation

Closes #7788
This commit is contained in:
Tunglies
2026-11-09 10:03:37 +08:00
parent 77a2df70c2
commit cd5156fb8a
5 changed files with 60 additions and 23 deletions
+1
View File
@@ -45,6 +45,7 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化连接、规则和日志筛选:切换页面后保留过滤条件
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
**🍎 macOS**
+38
View File
@@ -0,0 +1,38 @@
import { useCallback, useMemo, useState } from 'react'
import type { SearchState } from '@/components/base'
import { compileStringMatcher } from '@/utils/search-matcher'
const initialSearch: SearchState = {
text: '',
matchCase: false,
matchWholeWord: false,
useRegularExpression: false,
}
const searches: Record<'connections' | 'rules' | 'logs', SearchState> = {
connections: initialSearch,
rules: initialSearch,
logs: initialSearch,
}
const saveSearch = (page: keyof typeof searches, state: SearchState) => {
searches[page] = state
}
export const usePageSearch = (page: keyof typeof searches) => {
const [searchState, setSearchState] = useState(() => searches[page])
const match = useMemo(
() => compileStringMatcher(searchState.text, searchState).matcher,
[searchState],
)
const onSearch = useCallback(
(_match: (content: string) => boolean, state: SearchState) => {
saveSearch(page, state)
setSearchState(state)
},
[page],
)
return { searchState, match, onSearch }
}
+8 -13
View File
@@ -24,7 +24,6 @@ import {
BasePage,
BaseSearchBox,
BaseStyledSelect,
type SearchState,
VirtualList,
} from '@/components/base'
import {
@@ -39,6 +38,7 @@ import {
import { ConnectionTable } from '@/components/connection/connection-table'
import { useConnectionData } from '@/hooks/use-connection-data'
import { useConnectionSetting } from '@/hooks/use-connection-setting'
import { usePageSearch } from '@/hooks/use-page-search'
import { useTrafficData } from '@/hooks/use-traffic-data'
import { useVisibility } from '@/hooks/use-visibility'
import { mutate } from '@/services/mutate'
@@ -83,10 +83,8 @@ const EMPTY_CONNECTIONS: IConnectionsItem[] = []
const ConnectionsPage = () => {
const { t } = useTranslation()
const pageVisible = useVisibility()
const [match, setMatch] = useState<(input: string) => boolean>(
() => () => true,
)
const [hasSearch, setHasSearch] = useState(false)
const { searchState, match, onSearch } = usePageSearch('connections')
const hasSearch = searchState.text.length > 0
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
'active',
@@ -160,13 +158,6 @@ const ConnectionsPage = () => {
}),
)
const handleSearch = useCallback(
(match: (content: string) => boolean, state: SearchState) => {
setMatch(() => match)
setHasSearch(state.text.length > 0)
},
[],
)
const hasTableData = filterConn.length > 0
return (
@@ -274,7 +265,11 @@ const ConnectionsPage = () => {
},
}}
>
<BaseSearchBox onSearch={handleSearch} />
<BaseSearchBox
defaultValue={searchState.text}
{...searchState}
onSearch={onSearch}
/>
</Box>
{isTableLayout && hasTableData && (
<Tooltip title={t('connections.components.columnManager.title')}>
+6 -8
View File
@@ -4,7 +4,7 @@ import {
SwapVertRounded,
} from '@mui/icons-material'
import { Box, Button, IconButton, MenuItem } from '@mui/material'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import {
@@ -12,13 +12,13 @@ import {
BasePage,
BaseSearchBox,
BaseStyledSelect,
type SearchState,
VirtualList,
type VirtualListHandle,
} from '@/components/base'
import LogItem from '@/components/log/log-item'
import { useClashLog } from '@/hooks/use-clash-log'
import { useLogData } from '@/hooks/use-log-data'
import { usePageSearch } from '@/hooks/use-page-search'
const LogPage = () => {
const { t } = useTranslation()
@@ -28,8 +28,7 @@ const LogPage = () => {
const logOrder = clashLog.logOrder ?? 'asc'
const isDescending = logOrder === 'desc'
const [match, setMatch] = useState(() => (_: string) => true)
const [searchState, setSearchState] = useState<SearchState>()
const { searchState, match, onSearch } = usePageSearch('logs')
const {
response: { data: logData },
refreshGetClashLog,
@@ -174,10 +173,9 @@ const LogPage = () => {
<MenuItem value="err">{t('shared.filters.logLevels.error')}</MenuItem>
</BaseStyledSelect>
<BaseSearchBox
onSearch={(matcher, state) => {
setMatch(() => matcher)
setSearchState(state)
}}
defaultValue={searchState.text}
{...searchState}
onSearch={onSearch}
/>
</Box>
+7 -2
View File
@@ -12,6 +12,7 @@ import {
import { ScrollTopButton } from '@/components/layout/scroll-top-button'
import { ProviderButton } from '@/components/rule/provider-button'
import RuleItem from '@/components/rule/rule-item'
import { usePageSearch } from '@/hooks/use-page-search'
import { useVisibility } from '@/hooks/use-visibility'
import { useAppRefreshers, useRulesData } from '@/providers/app-data-context'
@@ -19,7 +20,7 @@ const RulesPage = () => {
const { t } = useTranslation()
const { rules = [] } = useRulesData()
const { refreshRules, refreshRuleProviders } = useAppRefreshers()
const [match, setMatch] = useState(() => (_: string) => true)
const { searchState, match, onSearch } = usePageSearch('rules')
const virtuosoRef = useRef<VirtualListHandle>(null)
const [showScrollTop, setShowScrollTop] = useState(false)
const pageVisible = useVisibility()
@@ -76,7 +77,11 @@ const RulesPage = () => {
alignItems: 'center',
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<BaseSearchBox
defaultValue={searchState.text}
{...searchState}
onSearch={onSearch}
/>
</Box>
{filteredRules && filteredRules.length > 0 ? (