mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-05 12:45:00 +03:00
1bdb517b88
Ant Design's Spin wraps its content in aria-live="polite", so every WebSocket tick on the dashboard, inbounds, clients and nodes pages was announced, and only the changed number was read, with no label. Pass aria-live="off" on every Spin that wraps content; aria-busy still signals loading. Each vital tile is now a group named by its metric, so "47.2 %" is read as CPU. rc-select renders each tag's remove "x" as an aria-hidden span with no tabIndex, so screen-reader users could only Backspace the last value away, never remove one SNI or inbound from the middle. selectTagRender swaps in a real button named "Remove <value>" and moves focus to the next tag after removal. It is applied to every tags/multiple Select. maxTagCount="responsive" is dropped because it hid selected inbounds behind "+N", where no screen reader could reach them. Two oxlint rules (antd-a11y/spin-content-not-live and antd-a11y/select-tag-remove) keep new code from regressing either fix.
203 lines
5.8 KiB
TypeScript
203 lines
5.8 KiB
TypeScript
import { useCallback, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Alert, Select, Switch } from 'antd';
|
|
|
|
import { selectTagRender } from '@/components/form';
|
|
import { SettingListItem } from '@/components/ui';
|
|
import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting';
|
|
import {
|
|
BITTORRENT_PROTOCOLS,
|
|
BLOCK_DOMAINS_OPTIONS,
|
|
DOMAINS_OPTIONS,
|
|
IPS_OPTIONS,
|
|
SERVICES_OPTIONS,
|
|
directSettings,
|
|
ipv4Settings,
|
|
} from '../basics/constants';
|
|
import {
|
|
getDefaultOutboundTag,
|
|
ruleGetter,
|
|
ruleSetter,
|
|
setDefaultOutboundTag,
|
|
syncOutbound,
|
|
} from '../basics/helpers';
|
|
|
|
interface RoutingBasicProps {
|
|
templateSettings: XraySettingsValue | null;
|
|
setTemplateSettings: SetTemplate;
|
|
}
|
|
|
|
export default function RoutingBasic({ templateSettings, setTemplateSettings }: RoutingBasicProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const mutate = useCallback(
|
|
(mutator: (next: XraySettingsValue) => void) => {
|
|
setTemplateSettings((prev) => {
|
|
if (!prev) return prev;
|
|
const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue;
|
|
mutator(clone);
|
|
return clone;
|
|
});
|
|
},
|
|
[setTemplateSettings],
|
|
);
|
|
|
|
const blockedIPs = ruleGetter(templateSettings, 'blocked', 'ip');
|
|
const blockedDomains = ruleGetter(templateSettings, 'blocked', 'domain');
|
|
const blockedProtocols = ruleGetter(templateSettings, 'blocked', 'protocol');
|
|
const directIPs = ruleGetter(templateSettings, 'direct', 'ip');
|
|
const directDomains = ruleGetter(templateSettings, 'direct', 'domain');
|
|
const ipv4Domains = ruleGetter(templateSettings, 'IPv4', 'domain');
|
|
|
|
const torrentActive = BITTORRENT_PROTOCOLS.every((p) => blockedProtocols.includes(p));
|
|
const defaultOutboundTag = getDefaultOutboundTag(templateSettings);
|
|
const defaultOutboundOptions = useMemo(() => {
|
|
const tags = new Set<string>(['direct', 'blocked']);
|
|
for (const o of templateSettings?.outbounds ?? []) {
|
|
if (o?.tag) tags.add(o.tag);
|
|
}
|
|
return [...tags].map((value) => ({ label: value, value }));
|
|
}, [templateSettings?.outbounds]);
|
|
|
|
return (
|
|
<>
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
className="mb-12 hint-alert"
|
|
title={t('pages.xray.blockConnectionsConfigsDesc')}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.defaultOutbound')}
|
|
description={t('pages.xray.defaultOutboundDesc')}
|
|
paddings="small"
|
|
control={
|
|
<Select
|
|
value={defaultOutboundTag}
|
|
style={{ width: '100%' }}
|
|
options={defaultOutboundOptions}
|
|
onChange={(tag) => mutate((tt) => setDefaultOutboundTag(tt, tag))}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.Torrent')}
|
|
paddings="small"
|
|
control={
|
|
<Switch
|
|
checked={torrentActive}
|
|
onChange={(checked) =>
|
|
mutate((tt) => {
|
|
const next = checked
|
|
? [...blockedProtocols, ...BITTORRENT_PROTOCOLS]
|
|
: blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d));
|
|
ruleSetter(tt, 'blocked', 'protocol', next);
|
|
})
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.blockips')}
|
|
paddings="small"
|
|
control={
|
|
<Select
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
value={blockedIPs}
|
|
style={{ width: '100%' }}
|
|
options={IPS_OPTIONS}
|
|
onChange={(v) => mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.blockdomains')}
|
|
paddings="small"
|
|
control={
|
|
<Select
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
value={blockedDomains}
|
|
style={{ width: '100%' }}
|
|
options={BLOCK_DOMAINS_OPTIONS}
|
|
onChange={(v) => mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
className="mb-12 hint-alert"
|
|
title={t('pages.xray.directConnectionsConfigsDesc')}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.directips')}
|
|
paddings="small"
|
|
control={
|
|
<Select
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
value={directIPs}
|
|
style={{ width: '100%' }}
|
|
options={IPS_OPTIONS}
|
|
onChange={(v) =>
|
|
mutate((tt) => {
|
|
ruleSetter(tt, 'direct', 'ip', v);
|
|
syncOutbound(tt, 'direct', directSettings);
|
|
})
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.directdomains')}
|
|
paddings="small"
|
|
control={
|
|
<Select
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
value={directDomains}
|
|
style={{ width: '100%' }}
|
|
options={DOMAINS_OPTIONS}
|
|
onChange={(v) =>
|
|
mutate((tt) => {
|
|
ruleSetter(tt, 'direct', 'domain', v);
|
|
syncOutbound(tt, 'direct', directSettings);
|
|
})
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<SettingListItem
|
|
title={t('pages.xray.ipv4Routing')}
|
|
description={t('pages.xray.ipv4RoutingDesc')}
|
|
paddings="small"
|
|
control={
|
|
<Select
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
value={ipv4Domains}
|
|
style={{ width: '100%' }}
|
|
options={SERVICES_OPTIONS}
|
|
onChange={(v) =>
|
|
mutate((tt) => {
|
|
ruleSetter(tt, 'IPv4', 'domain', v);
|
|
syncOutbound(tt, 'IPv4', ipv4Settings);
|
|
})
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
}
|