mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 23:42:27 +08:00
feat(proxies): add empty-state troubleshooting guidance
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import type { ProxyViewV1 } from '@/types/proxy-view'
|
||||
|
||||
export type ProxyEmptyStateReason =
|
||||
| 'no-subscriptions'
|
||||
| 'inactive-subscription'
|
||||
| 'core-unavailable'
|
||||
| 'no-proxy-info'
|
||||
|
||||
type ProfileSummary = {
|
||||
current?: string
|
||||
items?: Array<{
|
||||
uid?: string
|
||||
type?: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface ProxyEmptyStateInput {
|
||||
mode: string
|
||||
isChainMode: boolean
|
||||
profiles?: ProfileSummary
|
||||
isProfilesPending: boolean
|
||||
proxyView?: ProxyViewV1
|
||||
isProxyViewPending: boolean
|
||||
isProxyViewError: boolean
|
||||
runningMode?: string
|
||||
isRunningModePending: boolean
|
||||
}
|
||||
|
||||
const isSubscriptionProfile = (item: { type?: string }): boolean =>
|
||||
item.type === 'remote' || item.type === 'local'
|
||||
|
||||
export const hasRenderableProxyContent = (
|
||||
proxyView: ProxyViewV1 | undefined,
|
||||
mode: string,
|
||||
isChainMode: boolean,
|
||||
) => {
|
||||
if (!proxyView) return false
|
||||
|
||||
if (isChainMode) {
|
||||
return proxyView.groups.some(
|
||||
(group) => group.type === 'Selector' || group.type === 'URLTest',
|
||||
)
|
||||
}
|
||||
|
||||
if (mode === 'rule' || mode === 'script') {
|
||||
return proxyView.groups.some((group) => !group.hidden)
|
||||
}
|
||||
|
||||
return proxyView.global !== null
|
||||
}
|
||||
|
||||
export const resolveProxyEmptyState = ({
|
||||
mode,
|
||||
isChainMode,
|
||||
profiles,
|
||||
isProfilesPending,
|
||||
proxyView,
|
||||
isProxyViewPending,
|
||||
isProxyViewError,
|
||||
runningMode,
|
||||
isRunningModePending,
|
||||
}: ProxyEmptyStateInput): ProxyEmptyStateReason | 'loading' | null => {
|
||||
if (mode === 'direct') return null
|
||||
|
||||
if (isProfilesPending || isProxyViewPending || isRunningModePending) {
|
||||
return 'loading'
|
||||
}
|
||||
|
||||
if (profiles) {
|
||||
const subscriptions = (profiles.items ?? []).filter(isSubscriptionProfile)
|
||||
if (subscriptions.length === 0) return 'no-subscriptions'
|
||||
|
||||
const hasActiveSubscription = subscriptions.some(
|
||||
(item) => item.uid === profiles.current,
|
||||
)
|
||||
if (!hasActiveSubscription) return 'inactive-subscription'
|
||||
}
|
||||
|
||||
if (hasRenderableProxyContent(proxyView, mode, isChainMode)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (runningMode === 'NotRunning' || isProxyViewError) {
|
||||
return 'core-unavailable'
|
||||
}
|
||||
|
||||
return 'no-proxy-info'
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Alert, AlertTitle, Box, Button, Stack } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import { useAppRefreshers } from '@/providers/app-data-context'
|
||||
import { openLogsDir, restartCore } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { revalidateQuery } from '@/services/query-client'
|
||||
|
||||
import type { ProxyEmptyStateReason } from './proxy-empty-state-model'
|
||||
|
||||
interface Props {
|
||||
reason: ProxyEmptyStateReason
|
||||
}
|
||||
|
||||
export const ProxyEmptyState = ({ reason }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { refreshProxy } = useAppRefreshers()
|
||||
const [isRestarting, setIsRestarting] = useState(false)
|
||||
|
||||
const handleRestart = useLockFn(async () => {
|
||||
setIsRestarting(true)
|
||||
try {
|
||||
await restartCore()
|
||||
await Promise.all([refreshProxy(), revalidateQuery(['getRunningMode'])])
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
} finally {
|
||||
setIsRestarting(false)
|
||||
}
|
||||
})
|
||||
|
||||
const title =
|
||||
reason === 'no-subscriptions'
|
||||
? t('proxies.page.empty.noSubscriptions.title')
|
||||
: reason === 'inactive-subscription'
|
||||
? t('proxies.page.empty.inactiveSubscription.title')
|
||||
: reason === 'core-unavailable'
|
||||
? t('proxies.page.empty.coreUnavailable.title')
|
||||
: t('proxies.page.empty.noProxyInfo.title')
|
||||
const description =
|
||||
reason === 'no-subscriptions'
|
||||
? t('proxies.page.empty.noSubscriptions.description')
|
||||
: reason === 'inactive-subscription'
|
||||
? t('proxies.page.empty.inactiveSubscription.description')
|
||||
: reason === 'core-unavailable'
|
||||
? t('proxies.page.empty.coreUnavailable.description')
|
||||
: t('proxies.page.empty.noProxyInfo.description')
|
||||
const showProfiles =
|
||||
reason === 'no-subscriptions' ||
|
||||
reason === 'inactive-subscription' ||
|
||||
reason === 'no-proxy-info'
|
||||
const showCoreActions =
|
||||
reason === 'core-unavailable' || reason === 'no-proxy-info'
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
severity="warning"
|
||||
variant="outlined"
|
||||
sx={{ width: '100%', maxWidth: 640 }}
|
||||
>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
{description}
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
useFlexGap
|
||||
sx={{ mt: 2, flexWrap: 'wrap' }}
|
||||
>
|
||||
{showProfiles && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => navigate('/profile')}
|
||||
>
|
||||
{t('proxies.page.empty.actions.openProfiles')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showCoreActions && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
loading={isRestarting}
|
||||
onClick={() => void handleRestart()}
|
||||
>
|
||||
{t('proxies.page.empty.actions.restartCore')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
disabled={isRestarting}
|
||||
onClick={() => void openLogsDir()}
|
||||
>
|
||||
{t('proxies.page.empty.actions.openLogs')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Alert>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
StickyVirtualList,
|
||||
type StickyVirtualListHandle,
|
||||
} from '@/components/base'
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useProxiesData } from '@/providers/app-data-context'
|
||||
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
||||
import delayManager from '@/services/delay'
|
||||
import {
|
||||
isInteractableMember,
|
||||
@@ -30,6 +31,8 @@ import {
|
||||
} from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
import { ProxyEmptyState } from './proxy-empty-state'
|
||||
import { resolveProxyEmptyState } from './proxy-empty-state-model'
|
||||
import {
|
||||
DEFAULT_HOVER_DELAY,
|
||||
ProxyGroupNavigator,
|
||||
@@ -598,11 +601,34 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
|
||||
export const ProxyGroups = (props: Props) => {
|
||||
const { mode, isChainMode = false, chainConfigData } = props
|
||||
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
||||
const { proxyView, isProxyViewPending, isProxyViewError } = useProxiesData()
|
||||
const { runningMode, isRunningModePending } = useSystemData()
|
||||
|
||||
const emptyState = resolveProxyEmptyState({
|
||||
mode,
|
||||
isChainMode,
|
||||
profiles,
|
||||
isProfilesPending: !profiles && isProfilesLoading,
|
||||
proxyView,
|
||||
isProxyViewPending,
|
||||
isProxyViewError,
|
||||
runningMode,
|
||||
isRunningModePending,
|
||||
})
|
||||
|
||||
if (mode === 'direct') {
|
||||
return <BaseEmpty textKey="proxies.page.messages.directMode" />
|
||||
}
|
||||
|
||||
if (emptyState === 'loading') {
|
||||
return <BaseLoading />
|
||||
}
|
||||
|
||||
if (emptyState) {
|
||||
return <ProxyEmptyState reason={emptyState} />
|
||||
}
|
||||
|
||||
if (isChainMode) {
|
||||
return <ChainProxyGroups mode={mode} chainConfigData={chainConfigData} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user