mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 08:45:41 +08:00
347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
import { BaseDialog, Switch } from "@/components/base";
|
||
import { useClashInfo } from "@/hooks/use-clash";
|
||
import { useVerge } from "@/hooks/use-verge";
|
||
import { showNotice } from "@/services/noticeService";
|
||
import getSystem from "@/utils/get-system";
|
||
import { Shuffle } from "@mui/icons-material";
|
||
import {
|
||
CircularProgress,
|
||
IconButton,
|
||
List,
|
||
ListItem,
|
||
ListItemText,
|
||
Stack,
|
||
TextField
|
||
} from "@mui/material";
|
||
import { useLockFn, useRequest } from "ahooks";
|
||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
const OS = getSystem();
|
||
|
||
interface ClashPortViewerProps {}
|
||
|
||
interface ClashPortViewerRef {
|
||
open: () => void;
|
||
close: () => void;
|
||
}
|
||
|
||
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
|
||
|
||
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>(
|
||
(props, ref) => {
|
||
const { t } = useTranslation();
|
||
const { clashInfo, patchInfo } = useClashInfo();
|
||
const { verge, patchVerge } = useVerge();
|
||
const [open, setOpen] = useState(false);
|
||
|
||
// Mixed Port
|
||
const [mixedPort, setMixedPort] = useState(
|
||
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
|
||
);
|
||
|
||
// 其他端口状态
|
||
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);
|
||
|
||
// 添加保存请求,防止GUI卡死
|
||
const { loading, run: saveSettings } = useRequest(
|
||
async (params: {
|
||
clashConfig: any;
|
||
vergeConfig: any;
|
||
}) => {
|
||
const { clashConfig, vergeConfig } = params;
|
||
await Promise.all([
|
||
patchInfo(clashConfig),
|
||
patchVerge(vergeConfig)
|
||
]);
|
||
},
|
||
{
|
||
manual: true,
|
||
onSuccess: () => {
|
||
setOpen(false);
|
||
showNotice("success", t("Port settings saved")); // 调用提示函数
|
||
},
|
||
onError: () => {
|
||
showNotice("error", t("Failed to save settings")); // 调用提示函数
|
||
}
|
||
}
|
||
);
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
open: () => {
|
||
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
|
||
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);
|
||
setOpen( true);
|
||
},
|
||
close: () => setOpen(false),
|
||
}));
|
||
|
||
const onSave = useLockFn(async () => {
|
||
// 端口冲突检测
|
||
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) {
|
||
return;
|
||
}
|
||
|
||
// 验证端口范围
|
||
const isValidPort = (port: number) => port >= 1 && port <= 65535;
|
||
const allPortsValid = [
|
||
mixedPort,
|
||
socksEnabled ? socksPort : 0,
|
||
httpEnabled ? httpPort : 0,
|
||
redirEnabled ? redirPort : 0,
|
||
tproxyEnabled ? tproxyPort : 0
|
||
].every(port => port === 0 || isValidPort(port));
|
||
|
||
if (!allPortsValid) {
|
||
return;
|
||
}
|
||
|
||
// 准备配置数据
|
||
const clashConfig = {
|
||
"mixed-port": mixedPort,
|
||
"socks-port": socksPort,
|
||
port: httpPort,
|
||
"redir-port": redirPort,
|
||
"tproxy-port": tproxyPort
|
||
};
|
||
|
||
const vergeConfig = {
|
||
verge_mixed_port: mixedPort,
|
||
verge_socks_port: socksPort,
|
||
verge_socks_enabled: socksEnabled,
|
||
verge_port: httpPort,
|
||
verge_http_enabled: httpEnabled,
|
||
verge_redir_port: redirPort,
|
||
verge_redir_enabled: redirEnabled,
|
||
verge_tproxy_port: tproxyPort,
|
||
verge_tproxy_enabled: tproxyEnabled
|
||
};
|
||
|
||
// 提交保存请求
|
||
await saveSettings({ clashConfig, vergeConfig });
|
||
});
|
||
|
||
// 优化的数字输入处理
|
||
const handleNumericChange = (setter: (value: number) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const value = e.target.value.replace(/\D+/, '');
|
||
if (value === '') {
|
||
setter(0);
|
||
return;
|
||
}
|
||
|
||
const num = parseInt(value, 10);
|
||
if (!isNaN(num) && num >= 0 && num <= 65535) {
|
||
setter(num);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<BaseDialog
|
||
open={open}
|
||
title={t("Port Configuration")}
|
||
contentSx={{
|
||
width: 400
|
||
}}
|
||
okBtn={
|
||
loading ? (
|
||
<Stack direction="row" alignItems="center" spacing={1}>
|
||
<CircularProgress size={20} />
|
||
{t("Saving...")}
|
||
</Stack>
|
||
) : t("Save")
|
||
}
|
||
cancelBtn={t("Cancel")}
|
||
onClose={() => setOpen(false)}
|
||
onCancel={() => setOpen(false)}
|
||
onOk={onSave}
|
||
>
|
||
<List sx={{ width: "100%" }}>
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("Mixed Port")}
|
||
primaryTypographyProps={{ 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))}
|
||
inputProps={{ style: { fontSize: 12 } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setMixedPort(generateRandomPort())}
|
||
title={t("Random Port")}
|
||
sx={{ mr: 0.5 }}
|
||
>
|
||
<Shuffle fontSize="small" />
|
||
</IconButton>
|
||
<Switch
|
||
size="small"
|
||
checked={true}
|
||
disabled={true}
|
||
sx={{ ml: 0.5, opacity: 0.7 }}
|
||
/>
|
||
</div>
|
||
</ListItem>
|
||
|
||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||
<ListItemText
|
||
primary={t("Socks Port")}
|
||
primaryTypographyProps={{ 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}
|
||
inputProps={{ style: { fontSize: 12 } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setSocksPort(generateRandomPort())}
|
||
title={t("Random Port")}
|
||
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("HTTP Port")}
|
||
primaryTypographyProps={{ 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}
|
||
inputProps={{ style: { fontSize: 12 } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setHttpPort(generateRandomPort())}
|
||
title={t("Random Port")}
|
||
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("Redir Port")}
|
||
primaryTypographyProps={{ 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}
|
||
inputProps={{ style: { fontSize: 12 } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setRedirPort(generateRandomPort())}
|
||
title={t("Random Port")}
|
||
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("Tproxy Port")}
|
||
primaryTypographyProps={{ 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}
|
||
inputProps={{ style: { fontSize: 12 } }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => setTproxyPort(generateRandomPort())}
|
||
title={t("Random Port")}
|
||
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>
|
||
);
|
||
}
|
||
);
|