| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- import { useCallback } from 'react';
- import { useTranslation } from 'react-i18next';
- import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd';
- import {
- BarChartOutlined,
- ClockCircleOutlined,
- FileTextOutlined,
- ReloadOutlined,
- SettingOutlined,
- } from '@ant-design/icons';
- import { OutboundDomainStrategies } from '@/schemas/primitives';
- import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
- import { SettingListItem } from '@/components/ui';
- import { useMediaQuery } from '@/hooks/useMediaQuery';
- import { catTabLabel } from '@/pages/settings/catTabLabel';
- import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting';
- import './BasicsTab.css';
- import {
- ACCESS_LOG,
- ERROR_LOG,
- LOG_LEVELS,
- MASK_ADDRESS,
- ROUTING_DOMAIN_STRATEGIES,
- } from './constants';
- interface BasicsTabProps {
- templateSettings: XraySettingsValue | null;
- setTemplateSettings: SetTemplate;
- outboundTestUrl: string;
- onChangeOutboundTestUrl: (v: string) => void;
- onResetDefault: () => void;
- }
- export default function BasicsTab({
- templateSettings,
- setTemplateSettings,
- outboundTestUrl,
- onChangeOutboundTestUrl,
- onResetDefault,
- }: BasicsTabProps) {
- const { t } = useTranslation();
- const { isMobile } = useMediaQuery();
- const [modal, modalContextHolder] = Modal.useModal();
- 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 setLevel0 = useCallback(
- (field: string, value: number | null) => mutate((tt) => {
- if (!tt.policy) tt.policy = {};
- if (!tt.policy.levels) tt.policy.levels = {};
- if (!tt.policy.levels['0']) tt.policy.levels['0'] = {};
- if (value === null || value === undefined) {
- delete tt.policy.levels['0'][field];
- } else {
- tt.policy.levels['0'][field] = value;
- }
- }),
- [mutate],
- );
- const metricsCfg = (templateSettings as { metrics?: { tag?: string; listen?: string } } | null)?.metrics;
- const setMetrics = useCallback(
- (field: 'tag' | 'listen', value: string) => mutate((tt) => {
- const node = tt as { metrics?: { tag?: string; listen?: string }; stats?: Record<string, unknown> };
- const m: { tag?: string; listen?: string } = { ...(node.metrics ?? {}) };
- if (value.trim() === '') {
- delete m[field];
- } else {
- m[field] = value.trim();
- }
- if (!m.listen && !m.tag) {
- delete node.metrics;
- } else {
- node.metrics = m;
- // xray-core's metrics handler needs a stats object to populate.
- if (!node.stats) node.stats = {};
- }
- }),
- [mutate],
- );
- function confirmResetDefault() {
- modal.confirm({
- title: t('pages.settings.resetDefaultConfig'),
- okText: t('reset'),
- okType: 'danger',
- cancelText: t('cancel'),
- onOk: () => onResetDefault(),
- });
- }
- const freedomStrategy =
- (templateSettings?.outbounds?.find((o) => o?.protocol === 'freedom' && o?.tag === 'direct')?.settings as
- | { domainStrategy?: string }
- | undefined)?.domainStrategy ?? 'AsIs';
- const directFreedomOutbound = templateSettings?.outbounds?.find(
- (o) => o?.protocol === 'freedom' && o?.tag === 'direct',
- );
- const directHappyEyeballs = (() => {
- const sockopt = (directFreedomOutbound?.streamSettings as { sockopt?: { happyEyeballs?: unknown } } | undefined)
- ?.sockopt;
- const raw = sockopt?.happyEyeballs;
- if (raw == null || typeof raw !== 'object') return null;
- return HappyEyeballsSchema.parse(raw);
- })();
- const setDirectHappyEyeballs = useCallback(
- (next: ReturnType<typeof HappyEyeballsSchema.parse> | null) => {
- mutate((tt) => {
- if (!tt.outbounds) tt.outbounds = [];
- let idx = tt.outbounds.findIndex((o) => o?.protocol === 'freedom' && o?.tag === 'direct');
- if (idx < 0) {
- tt.outbounds.push({ protocol: 'freedom', tag: 'direct', settings: {} });
- idx = tt.outbounds.length - 1;
- }
- const ob = tt.outbounds[idx];
- const stream = (ob.streamSettings ?? {}) as Record<string, unknown>;
- const sockopt = (stream.sockopt ?? {}) as Record<string, unknown>;
- if (next == null) {
- delete sockopt.happyEyeballs;
- } else {
- sockopt.happyEyeballs = next;
- }
- if (Object.keys(sockopt).length === 0) {
- delete stream.sockopt;
- } else {
- stream.sockopt = sockopt;
- }
- if (Object.keys(stream).length === 0) {
- delete ob.streamSettings;
- } else {
- ob.streamSettings = stream;
- }
- });
- },
- [mutate],
- );
- const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs';
- const log = (templateSettings?.log || {}) as Record<string, unknown>;
- const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>;
- const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record<string, unknown>;
- const items = [
- {
- key: '1',
- label: catTabLabel(<SettingOutlined />, t('pages.xray.generalConfigs'), isMobile),
- children: (
- <>
- <Alert
- type="warning"
- showIcon
- className="mb-12 hint-alert"
- title={t('pages.xray.generalConfigsDesc')}
- />
- <SettingListItem
- title={t('pages.xray.FreedomStrategy')}
- description={t('pages.xray.FreedomStrategyDesc')}
- paddings="small"
- control={
- <Select
- value={freedomStrategy}
- style={{ width: '100%' }}
- options={OutboundDomainStrategies.map((s) => ({ value: s, label: s }))}
- onChange={(next) => mutate((tt) => {
- if (!tt.outbounds) tt.outbounds = [];
- const idx = tt.outbounds.findIndex((o) => o?.protocol === 'freedom' && o?.tag === 'direct');
- if (idx < 0) {
- tt.outbounds.push({ protocol: 'freedom', tag: 'direct', settings: { domainStrategy: next } });
- } else {
- const ob = tt.outbounds[idx];
- ob.settings = (ob.settings || {}) as Record<string, unknown>;
- (ob.settings as Record<string, unknown>).domainStrategy = next;
- }
- })}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.FreedomHappyEyeballs')}
- description={t('pages.xray.FreedomHappyEyeballsDesc')}
- paddings="small"
- control={
- <Switch
- checked={directHappyEyeballs != null}
- onChange={(checked) => {
- setDirectHappyEyeballs(checked ? HappyEyeballsSchema.parse({}) : null);
- }}
- />
- }
- />
- {directHappyEyeballs != null && (
- <>
- <SettingListItem
- title={t('pages.inbounds.form.tryDelayMs')}
- description={t('pages.xray.FreedomHappyEyeballsTryDelayDesc')}
- paddings="small"
- control={
- <InputNumber
- min={0}
- style={{ width: '100%' }}
- value={directHappyEyeballs.tryDelayMs}
- placeholder="150"
- onChange={(v) => setDirectHappyEyeballs({
- ...directHappyEyeballs,
- tryDelayMs: typeof v === 'number' ? v : 0,
- })}
- />
- }
- />
- <SettingListItem
- title={t('pages.inbounds.form.prioritizeIPv6')}
- paddings="small"
- control={
- <Switch
- checked={directHappyEyeballs.prioritizeIPv6}
- onChange={(checked) => setDirectHappyEyeballs({
- ...directHappyEyeballs,
- prioritizeIPv6: checked,
- })}
- />
- }
- />
- </>
- )}
- <SettingListItem
- title={t('pages.xray.RoutingStrategy')}
- description={t('pages.xray.RoutingStrategyDesc')}
- paddings="small"
- control={
- <Select
- value={routingStrategy}
- style={{ width: '100%' }}
- options={ROUTING_DOMAIN_STRATEGIES.map((s) => ({ value: s, label: s }))}
- onChange={(next) => mutate((tt) => {
- if (tt.routing) tt.routing.domainStrategy = next;
- })}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.outboundTestUrl')}
- description={t('pages.xray.outboundTestUrlDesc')}
- paddings="small"
- control={
- <Input
- value={outboundTestUrl}
- onChange={(e) => onChangeOutboundTestUrl(e.target.value)}
- placeholder="https://www.google.com/generate_204"
- />
- }
- />
- </>
- ),
- },
- {
- key: '2',
- label: catTabLabel(<BarChartOutlined />, t('pages.xray.statistics'), isMobile),
- children: (
- <>
- {[
- ['statsInboundUplink', t('pages.xray.statsInboundUplink')],
- ['statsInboundDownlink', t('pages.xray.statsInboundDownlink')],
- ['statsOutboundUplink', t('pages.xray.statsOutboundUplink')],
- ['statsOutboundDownlink', t('pages.xray.statsOutboundDownlink')],
- ].map(([field, label]) => (
- <SettingListItem
- key={field}
- title={label}
- paddings="small"
- control={
- <Switch
- checked={!!policy[field]}
- onChange={(checked) => mutate((tt) => {
- if (!tt.policy) tt.policy = {};
- if (!tt.policy.system) tt.policy.system = {};
- tt.policy.system[field] = checked;
- })}
- />
- }
- />
- ))}
- <SettingListItem
- title={t('pages.xray.metricsListen')}
- description={t('pages.xray.metricsListenDesc')}
- paddings="small"
- control={
- <Input
- value={metricsCfg?.listen ?? ''}
- onChange={(e) => setMetrics('listen', e.target.value)}
- placeholder="127.0.0.1:11111"
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.metricsTag')}
- paddings="small"
- control={
- <Input
- value={metricsCfg?.tag ?? ''}
- onChange={(e) => setMetrics('tag', e.target.value)}
- placeholder="metrics_out"
- />
- }
- />
- </>
- ),
- },
- {
- key: 'connection',
- label: catTabLabel(<ClockCircleOutlined />, t('pages.xray.connectionLimits'), isMobile),
- children: (
- <>
- <Alert
- type="warning"
- showIcon
- className="mb-12 hint-alert"
- title={t('pages.xray.connectionLimitsDesc')}
- />
- <SettingListItem
- title={t('pages.xray.connIdle')}
- description={t('pages.xray.connIdleDesc')}
- paddings="small"
- control={
- <InputNumber
- value={typeof level0.connIdle === 'number' ? level0.connIdle : undefined}
- min={0}
- style={{ width: '100%' }}
- placeholder="300"
- suffix={t('pages.xray.seconds')}
- onChange={(v) => setLevel0('connIdle', v as number | null)}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.bufferSize')}
- description={t('pages.xray.bufferSizeDesc')}
- paddings="small"
- control={
- <InputNumber
- value={typeof level0.bufferSize === 'number' ? level0.bufferSize : undefined}
- min={0}
- style={{ width: '100%' }}
- placeholder={t('pages.xray.bufferSizePlaceholder')}
- suffix="KB"
- onChange={(v) => setLevel0('bufferSize', v as number | null)}
- />
- }
- />
- </>
- ),
- },
- {
- key: '3',
- label: catTabLabel(<FileTextOutlined />, t('pages.xray.logConfigs'), isMobile),
- children: (
- <>
- <Alert
- type="warning"
- showIcon
- className="mb-12 hint-alert"
- title={t('pages.xray.logConfigsDesc')}
- />
- <SettingListItem
- title={t('pages.xray.logLevel')}
- description={t('pages.xray.logLevelDesc')}
- paddings="small"
- control={
- <Select
- value={(log.loglevel as string) || 'warning'}
- style={{ width: '100%' }}
- options={LOG_LEVELS.map((s) => ({ value: s, label: s }))}
- onChange={(v) => mutate((tt) => { if (tt.log) tt.log.loglevel = v; })}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.accessLog')}
- description={t('pages.xray.accessLogDesc')}
- paddings="small"
- control={
- <Select
- value={(log.access as string) || ''}
- style={{ width: '100%' }}
- options={ACCESS_LOG.map((s) => ({ value: s, label: s }))}
- onChange={(v) => mutate((tt) => { if (tt.log) tt.log.access = v; })}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.errorLog')}
- description={t('pages.xray.errorLogDesc')}
- paddings="small"
- control={
- <Select
- value={(log.error as string) || ''}
- style={{ width: '100%' }}
- options={[{ value: '', label: t('empty') }, ...ERROR_LOG.map((s) => ({ value: s, label: s }))]}
- onChange={(v) => mutate((tt) => { if (tt.log) tt.log.error = v; })}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.maskAddress')}
- description={t('pages.xray.maskAddressDesc')}
- paddings="small"
- control={
- <Select
- value={(log.maskAddress as string) || ''}
- style={{ width: '100%' }}
- options={[{ value: '', label: t('empty') }, ...MASK_ADDRESS.map((s) => ({ value: s, label: s }))]}
- onChange={(v) => mutate((tt) => { if (tt.log) tt.log.maskAddress = v; })}
- />
- }
- />
- <SettingListItem
- title={t('pages.xray.dnsLog')}
- description={t('pages.xray.dnsLogDesc')}
- paddings="small"
- control={
- <Switch
- checked={!!log.dnsLog}
- onChange={(v) => mutate((tt) => { if (tt.log) tt.log.dnsLog = v; })}
- />
- }
- />
- </>
- ),
- },
- {
- key: 'reset',
- label: catTabLabel(<ReloadOutlined />, t('pages.settings.resetDefaultConfig'), isMobile),
- children: (
- <Space style={{ padding: '0 20px' }}>
- <Button type="primary" danger icon={<ReloadOutlined />} onClick={confirmResetDefault}>
- {t('pages.settings.resetDefaultConfig')}
- </Button>
- </Space>
- ),
- },
- ];
- return (
- <>
- {modalContextHolder}
- <Tabs defaultActiveKey="1" items={items} />
- </>
- );
- }
|