refactor(port): build the port dialog rows from one listener table

This commit is contained in:
wonfen
2026-10-05 22:00:54 +08:00
parent 7cc6c1ed95
commit 30f05a7a39
2 changed files with 160 additions and 229 deletions
+1
View File
@@ -23,6 +23,7 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化端口设置窗口的实现,精简重复代码
- 优化侧边栏流量图表的 CPU 占用
**🖥️ Windows**
+150 -220
View File
@@ -16,8 +16,9 @@ import { BaseDialog, Switch } from '@/components/base'
import { useClashInfo } from '@/hooks/use-clash'
import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
import { useVerge } from '@/hooks/use-verge'
import { saveProxyPorts } from '@/services/cmds'
import { type ProxyPortSettings, saveProxyPorts } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import type { TranslationKey } from '@/types/generated/i18n-keys'
import getSystem from '@/utils/get-system'
const OS = getSystem()
@@ -27,9 +28,125 @@ interface ClashPortViewerRef {
close: () => void
}
type ListenerKey = Exclude<keyof ProxyPortSettings, 'mixedPort'>
const LISTENERS: {
key: ListenerKey
label: TranslationKey
visible: boolean
}[] = [
{
key: 'socks',
label: 'settings.modals.clashPort.fields.socks',
visible: true,
},
{
key: 'http',
label: 'settings.modals.clashPort.fields.http',
visible: true,
},
{
key: 'redir',
label: 'settings.modals.clashPort.fields.redir',
visible: OS !== 'windows',
},
{
key: 'tproxy',
label: 'settings.modals.clashPort.fields.tproxy',
visible: OS === 'linux',
},
]
const generateRandomPort = () =>
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025
const isValidPort = (port: number) => port >= 1 && port <= 65535
const readPortSettings = (
verge: IVergeConfig | undefined,
mixedPort: number,
): ProxyPortSettings => ({
mixedPort,
socks: {
enabled: verge?.verge_socks_enabled ?? false,
port: verge?.verge_socks_port ?? 7898,
},
http: {
enabled: verge?.verge_http_enabled ?? false,
port: verge?.verge_port ?? 7899,
},
redir: {
enabled: verge?.verge_redir_enabled ?? false,
port: verge?.verge_redir_port ?? 7895,
},
tproxy: {
enabled: verge?.verge_tproxy_enabled ?? false,
port: verge?.verge_tproxy_port ?? 7896,
},
})
interface PortRowProps {
label: string
secondary?: string
port: number
enabled: boolean
onPortChange: (port: number) => void
// Without it the switch stays locked on, as the mixed port cannot be disabled.
onEnabledChange?: (enabled: boolean) => void
}
const PortRow = ({
label,
secondary,
port,
enabled,
onPortChange,
onEnabledChange,
}: PortRowProps) => {
const { t } = useTranslation()
return (
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItemText
primary={label}
secondary={secondary}
slotProps={{
primary: { sx: { fontSize: 12 } },
secondary: { sx: { fontSize: 12 } },
}}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={port}
onChange={(e) =>
onPortChange(+e.target.value?.replace(/\D+/, '').slice(0, 5))
}
disabled={!enabled}
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
/>
<IconButton
size="small"
onClick={() => onPortChange(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
disabled={!enabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={enabled}
disabled={!onEnabledChange}
onChange={(_, c) => onEnabledChange?.(c)}
sx={{ ml: 0.5, opacity: onEnabledChange ? undefined : 0.7 }}
/>
</div>
</ListItem>
)
}
export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
const { t } = useTranslation()
const { verge } = useVerge()
@@ -38,27 +155,14 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
const displayedMixedPort = useDisplayedMixedPort()
const [open, setOpen] = useState(false)
const [ports, setPorts] = useState(() =>
readPortSettings(verge, configuredMixedPort),
)
// Mixed Port
const [mixedPort, setMixedPort] = useState(configuredMixedPort)
// 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898)
const [socksEnabled, setSocksEnabled] = useState(
verge?.verge_socks_enabled ?? false,
)
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899)
const [httpEnabled, setHttpEnabled] = useState(
verge?.verge_http_enabled ?? false,
)
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895)
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? false,
)
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896)
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? false,
)
const updateListener = (
key: ListenerKey,
patch: Partial<ProxyPortSettings[ListenerKey]>,
) => setPorts((prev) => ({ ...prev, [key]: { ...prev[key], ...patch } }))
// 添加保存请求,防止GUI卡死
const { loading, runAsync: saveSettings } = useRequest(saveProxyPorts, {
@@ -80,51 +184,33 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(configuredMixedPort)
setSocksPort(verge?.verge_socks_port ?? 7898)
setSocksEnabled(verge?.verge_socks_enabled ?? false)
setHttpPort(verge?.verge_port ?? 7899)
setHttpEnabled(verge?.verge_http_enabled ?? false)
setRedirPort(verge?.verge_redir_port ?? 7895)
setRedirEnabled(verge?.verge_redir_enabled ?? false)
setTproxyPort(verge?.verge_tproxy_port ?? 7896)
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false)
setPorts(readPortSettings(verge, configuredMixedPort))
setOpen(true)
},
close: () => setOpen(false),
}))
// TODO 减少代码复杂度,性能开支
const onSave = useLockFn(async () => {
const listeners = LISTENERS.map(({ key }) => ports[key])
// The backend also rejects out-of-range ports on disabled listeners.
const isValidPort = (port: number) => port >= 1 && port <= 65535
const allPorts = [mixedPort, socksPort, httpPort, redirPort, tproxyPort]
const allPorts = [ports.mixedPort, ...listeners.map(({ port }) => port)]
if (!allPorts.every(isValidPort)) {
showNotice.error('settings.modals.clashPort.messages.invalidPort')
return
}
// 端口冲突检测
const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1,
].filter((p) => p !== -1)
if (new Set(portList).size !== portList.length) {
const enabledPorts = [
ports.mixedPort,
...listeners.filter(({ enabled }) => enabled).map(({ port }) => port),
]
if (new Set(enabledPorts).size !== enabledPorts.length) {
showNotice.error('settings.modals.clashPort.messages.duplicatePort')
return
}
await saveSettings({
mixedPort,
socks: { enabled: socksEnabled, port: socksPort },
http: { enabled: httpEnabled, port: httpPort },
redir: { enabled: redirEnabled, port: redirPort },
tproxy: { enabled: tproxyEnabled, port: tproxyPort },
})
await saveSettings(ports)
})
return (
@@ -150,9 +236,8 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
onOk={onSave}
>
<List sx={{ width: '100%' }}>
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItemText
primary={t('settings.modals.clashPort.fields.mixed')}
<PortRow
label={t('settings.modals.clashPort.fields.mixed')}
secondary={
displayedMixedPort !== configuredMixedPort
? t('settings.modals.clashPort.messages.runningPort', {
@@ -160,177 +245,22 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
})
: undefined
}
slotProps={{
primary: { sx: { fontSize: 12 } },
secondary: { sx: { fontSize: 12 } },
}}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, '').slice(0, 5))
port={ports.mixedPort}
enabled
onPortChange={(mixedPort) =>
setPorts((prev) => ({ ...prev, mixedPort }))
}
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
/>
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
{LISTENERS.filter(({ visible }) => visible).map(({ key, label }) => (
<PortRow
key={key}
label={t(label)}
port={ports[key].port}
enabled={ports[key].enabled}
onPortChange={(port) => updateListener(key, { port })}
onEnabledChange={(enabled) => updateListener(key, { enabled })}
/>
</div>
</ListItem>
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItemText
primary={t('settings.modals.clashPort.fields.socks')}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, '').slice(0, 5))
}
disabled={!socksEnabled}
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItemText
primary={t('settings.modals.clashPort.fields.http')}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) =>
setHttpPort(+e.target.value?.replace(/\D+/, '').slice(0, 5))
}
disabled={!httpEnabled}
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== 'windows' && (
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItemText
primary={t('settings.modals.clashPort.fields.redir')}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={redirPort}
onChange={(e) =>
setRedirPort(+e.target.value?.replace(/\D+/, '').slice(0, 5))
}
disabled={!redirEnabled}
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === 'linux' && (
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItemText
primary={t('settings.modals.clashPort.fields.tproxy')}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={tproxyPort}
onChange={(e) =>
setTproxyPort(+e.target.value?.replace(/\D+/, '').slice(0, 5))
}
disabled={!tproxyEnabled}
slotProps={{ htmlInput: { style: { fontSize: 12 } } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
))}
</List>
</BaseDialog>
)