This commit is contained in:
Den Piligrim
2026-03-21 13:24:47 +03:00
parent 21ae964012
commit 5b2870e04a
17 changed files with 386 additions and 152 deletions
+92 -90
View File
@@ -18,6 +18,7 @@ interface Subscription {
name: string;
uuid: string;
inbounds: any[];
inboundsConfig?: any[];
}
interface Tunnel {
@@ -33,6 +34,7 @@ interface InboundConfigUI {
type: string;
port: string;
sni: string;
link?: string;
}
interface Domain { id: number; name: string; }
@@ -46,6 +48,7 @@ const CONNECTION_OPTIONS = [
'vmess-tcp',
'shadowsocks-tcp',
'trojan-tcp-reality',
'custom',
];
const patchLink = function (link: string, newHost: string): string {
@@ -124,16 +127,16 @@ export default function SubscriptionsPage() {
setEditingId(null);
setName('');
setInbounds([
{ id: generateId(), type: 'hysteria2-udp', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-xhttp-reality', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-grpc-reality', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vless-ws', port: 'random', sni: 'random' },
{ id: generateId(), type: 'vmess-tcp', port: 'random', sni: 'random' },
{ id: generateId(), type: 'shadowsocks-tcp', port: 'random', sni: 'random' },
{ id: generateId(), type: 'hysteria2-udp', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-xhttp-reality', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-grpc-reality', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vless-ws', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'vmess-tcp', port: 'random', sni: 'random', link: '' },
{ id: generateId(), type: 'shadowsocks-tcp', port: 'random', sni: 'random', link: '' },
]);
setPortErrors({});
setOpen(true);
@@ -143,79 +146,58 @@ export default function SubscriptionsPage() {
setEditingId(sub.id);
setName(sub.name);
// Пытаемся маппить существующие инбаунды, или даем дефолтные
if (sub.inbounds && sub.inbounds.length > 0) {
setInbounds(sub.inbounds.map(i => ({
if (sub.inboundsConfig && sub.inboundsConfig.length > 0) {
setInbounds(sub.inboundsConfig.map(i => ({
id: generateId(),
type: i.type || 'vless-tcp-reality',
port: i.port ? i.port.toString() : 'random',
sni: i.sni || 'random'
sni: i.sni || 'random',
link: i.link || ''
})));
} else {
setInbounds([{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }]);
setInbounds([{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }]);
}
setPortErrors({});
setOpen(true);
};
// Проверка порта
const handlePortCheck = async (portValue: string, id: string) => {
if (portValue === 'random' || portValue.trim() === '') {
setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; });
return;
}
const portNum = parseInt(portValue);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
setPortErrors(prev => ({ ...prev, [id]: 'Некорректный порт' }));
return;
}
try {
const { data } = await api.get(`/tunnels/check-port/${portNum}`);
if (!data.isFree) {
setPortErrors(prev => ({ ...prev, [id]: 'Порт занят' }));
} else {
setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; });
}
} catch (e) {
console.error('Ошибка проверки порта', e);
}
};
const handleInboundChange = (id: string, field: keyof InboundConfigUI, value: string) => {
setInbounds(prev => prev.map(inb => inb.id === id ? { ...inb, [field]: value } : inb));
if (field === 'port') {
if (field === 'port' || (field === 'type' && value === 'custom')) {
setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; });
}
};
const addInbound = () => {
if (inbounds.length < 20) {
setInbounds([...inbounds, { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }]);
setInbounds([...inbounds, { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }]);
}
};
const removeInbound = (id?: string) => {
if (id) {
if (inbounds.length > 1) {
setInbounds(inbounds.filter(inb => inb.id !== id));
setPortErrors(prev => {
const n = { ...prev };
delete n[id];
return n;
});
}
} else {
setInbounds([
{
id: crypto.randomUUID(),
type: 'vless-tcp-reality',
port: 'random',
sni: 'random'
const removeInbound = (id?: string) => {
if (id) {
if (inbounds.length > 1) {
setInbounds(inbounds.filter(inb => inb.id !== id));
setPortErrors(prev => {
const n = { ...prev };
delete n[id];
return n;
});
}
]);
setPortErrors({});
}
};
} else {
setInbounds([
{
id: crypto.randomUUID(),
type: 'vless-tcp-reality',
port: 'random',
sni: 'random',
link: ''
}
]);
setPortErrors({});
}
};
const handleSave = async () => {
if (Object.keys(portErrors).length > 0) {
@@ -229,11 +211,16 @@ const removeInbound = (id?: string) => {
const payload = {
name,
inboundsConfig: inbounds.map(i => ({
type: i.type,
port: i.port === 'random' ? 'random' : parseInt(i.port),
sni: i.sni
}))
inboundsConfig: inbounds.map(i => {
if (i.type === 'custom') {
return { type: i.type, link: i.link };
}
return {
type: i.type,
port: i.port === 'random' ? 'random' : parseInt(i.port),
sni: i.sni
};
})
};
try {
@@ -421,31 +408,46 @@ const removeInbound = (id?: string) => {
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 150 }}>
<TextField
size="small"
label="Порт"
placeholder="random или порт"
value={inb.port}
onChange={(e) => handleInboundChange(inb.id, 'port', e.target.value)}
onBlur={(e) => handlePortCheck(e.target.value, inb.id)}
error={!!portErrors[inb.id]}
helperText={portErrors[inb.id] || ""}
sx={{ width: 140 }}
/>
</FormControl>
{inb.type === 'custom' ? (
// Поле для кастомной ссылки
<FormControl size="small" sx={{ flexGrow: 1 }}>
<TextField
size="small"
label="Ссылка на подключение"
placeholder="vless://..."
value={inb.link || ''}
onChange={(e) => handleInboundChange(inb.id, 'link', e.target.value)}
fullWidth
/>
</FormControl>
) : (
<>
<FormControl size="small" sx={{ minWidth: 150 }}>
<TextField
size="small"
label="Порт"
placeholder="random или порт"
value={inb.port}
onChange={(e) => handleInboundChange(inb.id, 'port', e.target.value)}
error={!!portErrors[inb.id]}
helperText={portErrors[inb.id] || ""}
sx={{ width: 140 }}
/>
</FormControl>
<FormControl size="small" sx={{ minWidth: 150 }}>
<InputLabel>SNI</InputLabel>
<Select
value={inb.sni}
label="SNI"
onChange={(e) => handleInboundChange(inb.id, 'sni', e.target.value)}
>
<MenuItem value="random">random</MenuItem>
{domains.map(opt => <MenuItem key={opt.id} value={opt.name}>{opt.name}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 150 }}>
<InputLabel>SNI</InputLabel>
<Select
value={inb.sni}
label="SNI"
onChange={(e) => handleInboundChange(inb.id, 'sni', e.target.value)}
>
<MenuItem value="random">random</MenuItem>
{domains.map(opt => <MenuItem key={opt.id} value={opt.name}>{opt.name}</MenuItem>)}
</Select>
</FormControl>
</>
)}
<IconButton
color="primary"