mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d8721093e4
commit
b02091d598
1 changed files with 721 additions and 136 deletions
|
|
@ -1,196 +1,781 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
import { SyncOutlined, PlusOutlined, MinusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
import { Protocols, OutboundDomainStrategies } from '@/models/outbound.js';
|
import { Wireguard } from '@/utils';
|
||||||
|
import {
|
||||||
|
Outbound,
|
||||||
|
Protocols,
|
||||||
|
SSMethods,
|
||||||
|
TLS_FLOW_CONTROL,
|
||||||
|
UTLS_FINGERPRINT,
|
||||||
|
ALPN_OPTION,
|
||||||
|
SNIFFING_OPTION,
|
||||||
|
USERS_SECURITY,
|
||||||
|
OutboundDomainStrategies,
|
||||||
|
WireguardDomainStrategy,
|
||||||
|
Address_Port_Strategy,
|
||||||
|
MODE_OPTION,
|
||||||
|
DNSRuleActions,
|
||||||
|
} from '@/models/outbound.js';
|
||||||
|
|
||||||
// Outbound add/edit modal. The legacy modal is huge (1.3k lines)
|
const { t } = useI18n();
|
||||||
// because it covers every protocol's nested settings/streamSettings
|
|
||||||
// inline. We take the same pragmatic approach we did for the inbound
|
// Structured outbound add/edit modal — mirrors the legacy
|
||||||
// modal: a Basics tab covers the always-relevant fields (tag,
|
// web/html/form/outbound.html. Covers every protocol + transport
|
||||||
// protocol, sendThrough, domain strategy) and a JSON tab exposes
|
// combination the legacy panel exposes; the JSON tab still lets
|
||||||
// the full settings + streamSettings trees verbatim. Full structured
|
// power-users hand-edit fields the structured form doesn't surface
|
||||||
// per-protocol forms can land later — the JSON path supports every
|
// (deep finalmask/quic tuning, reverse-sniffing, etc.).
|
||||||
// field today and matches what the Advanced page-level JSON tab
|
|
||||||
// already does.
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
open: { type: Boolean, default: false },
|
open: { type: Boolean, default: false },
|
||||||
// null when adding, the outbound object when editing.
|
|
||||||
outbound: { type: Object, default: null },
|
outbound: { type: Object, default: null },
|
||||||
// Existing tags so we can flag duplicates client-side.
|
|
||||||
existingTags: { type: Array, default: () => [] },
|
existingTags: { type: Array, default: () => [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:open', 'confirm']);
|
const emit = defineEmits(['update:open', 'confirm']);
|
||||||
|
|
||||||
const PROTOCOL_OPTIONS = Object.values(Protocols);
|
const PROTOCOL_OPTIONS = Object.values(Protocols);
|
||||||
|
const SECURITY_OPTIONS = Object.values(USERS_SECURITY);
|
||||||
|
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||||
|
const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT);
|
||||||
|
const ALPN_OPTIONS = Object.values(ALPN_OPTION);
|
||||||
|
const NETWORKS = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
|
||||||
|
const NETWORK_LABELS = {
|
||||||
|
tcp: 'TCP (RAW)',
|
||||||
|
kcp: 'mKCP',
|
||||||
|
ws: 'WebSocket',
|
||||||
|
grpc: 'gRPC',
|
||||||
|
httpupgrade: 'HTTPUpgrade',
|
||||||
|
xhttp: 'XHTTP',
|
||||||
|
};
|
||||||
|
|
||||||
const form = reactive({
|
// Reactive draft — Outbound instance built from the prop on open.
|
||||||
tag: '',
|
const outbound = ref(null);
|
||||||
protocol: Protocols.Freedom,
|
|
||||||
sendThrough: '',
|
|
||||||
domainStrategy: 'AsIs',
|
|
||||||
settingsText: '',
|
|
||||||
streamSettingsText: '',
|
|
||||||
});
|
|
||||||
const isEdit = ref(false);
|
const isEdit = ref(false);
|
||||||
|
const activeKey = ref('1');
|
||||||
|
const linkInput = ref('');
|
||||||
|
|
||||||
function pretty(value) {
|
// Advanced JSON editor — kept in sync with the parsed Outbound on tab
|
||||||
if (value === null || value === undefined) return '';
|
// switch so users can copy/paste a full JSON config when the structured
|
||||||
if (typeof value === 'string') {
|
// form doesn't reach a field.
|
||||||
try { return JSON.stringify(JSON.parse(value), null, 2); }
|
const advancedJson = ref('');
|
||||||
catch (_e) { return value; }
|
|
||||||
}
|
|
||||||
try { return JSON.stringify(value, null, 2); }
|
|
||||||
catch (_e) { return ''; }
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.open, (next) => {
|
watch(() => props.open, (next) => {
|
||||||
if (!next) return;
|
if (!next) return;
|
||||||
if (props.outbound) {
|
if (props.outbound) {
|
||||||
isEdit.value = true;
|
isEdit.value = true;
|
||||||
const o = props.outbound;
|
outbound.value = Outbound.fromJson(props.outbound);
|
||||||
form.tag = o.tag || '';
|
|
||||||
form.protocol = o.protocol || Protocols.Freedom;
|
|
||||||
form.sendThrough = o.sendThrough || '';
|
|
||||||
form.domainStrategy = o.domainStrategy || 'AsIs';
|
|
||||||
form.settingsText = pretty(o.settings);
|
|
||||||
form.streamSettingsText = pretty(o.streamSettings);
|
|
||||||
} else {
|
} else {
|
||||||
isEdit.value = false;
|
isEdit.value = false;
|
||||||
form.tag = '';
|
outbound.value = new Outbound();
|
||||||
form.protocol = Protocols.Freedom;
|
|
||||||
form.sendThrough = '';
|
|
||||||
form.domainStrategy = 'AsIs';
|
|
||||||
form.settingsText = '';
|
|
||||||
form.streamSettingsText = '';
|
|
||||||
}
|
}
|
||||||
|
activeKey.value = '1';
|
||||||
|
linkInput.value = '';
|
||||||
|
primeAdvancedJson();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(activeKey, (key) => {
|
||||||
|
if (key === '2') primeAdvancedJson();
|
||||||
|
});
|
||||||
|
|
||||||
|
function primeAdvancedJson() {
|
||||||
|
if (!outbound.value) { advancedJson.value = ''; return; }
|
||||||
|
try {
|
||||||
|
advancedJson.value = JSON.stringify(outbound.value.toJson(), null, 2);
|
||||||
|
} catch (_e) {
|
||||||
|
advancedJson.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function close() { emit('update:open', false); }
|
function close() { emit('update:open', false); }
|
||||||
|
|
||||||
function buildResult() {
|
function onProtocolChange(next) {
|
||||||
// Empty JSON tabs collapse to undefined keys so the wire shape
|
if (!outbound.value) return;
|
||||||
// doesn't carry empty objects we never had in the first place.
|
outbound.value.protocol = next;
|
||||||
let settings;
|
|
||||||
let streamSettings;
|
|
||||||
try {
|
|
||||||
settings = form.settingsText.trim() ? JSON.parse(form.settingsText) : undefined;
|
|
||||||
} catch (e) {
|
|
||||||
message.error(`settings JSON invalid: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
streamSettings = form.streamSettingsText.trim()
|
|
||||||
? JSON.parse(form.streamSettingsText)
|
|
||||||
: undefined;
|
|
||||||
} catch (e) {
|
|
||||||
message.error(`streamSettings JSON invalid: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
const out = {
|
|
||||||
tag: form.tag,
|
|
||||||
protocol: form.protocol,
|
|
||||||
};
|
|
||||||
if (form.sendThrough) out.sendThrough = form.sendThrough;
|
|
||||||
if (form.domainStrategy && form.domainStrategy !== 'AsIs') {
|
|
||||||
out.domainStrategy = form.domainStrategy;
|
|
||||||
}
|
|
||||||
if (settings !== undefined) out.settings = settings;
|
|
||||||
if (streamSettings !== undefined) out.streamSettings = streamSettings;
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function streamNetworkChange(next) {
|
||||||
|
if (!outbound.value?.stream) return;
|
||||||
|
outbound.value.stream.network = next;
|
||||||
|
if (!outbound.value.canEnableTls()) outbound.value.stream.security = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateTag = computed(() => {
|
||||||
|
if (!outbound.value?.tag) return false;
|
||||||
|
const myTag = outbound.value.tag.trim();
|
||||||
|
if (!myTag) return false;
|
||||||
|
if (isEdit.value && props.outbound?.tag === myTag) return false;
|
||||||
|
return (props.existingTags || []).includes(myTag);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============== Submit ==============
|
||||||
function onOk() {
|
function onOk() {
|
||||||
if (!form.tag.trim()) {
|
if (!outbound.value) return;
|
||||||
message.error('Tag is required.');
|
if (!outbound.value.tag?.trim()) {
|
||||||
|
message.error(t('somethingWentWrong'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Block tag collisions client-side — server enforces too but this
|
if (duplicateTag.value) {
|
||||||
// surfaces faster.
|
message.error(t('somethingWentWrong'));
|
||||||
const conflict = (props.existingTags || []).includes(form.tag.trim());
|
|
||||||
if (conflict) {
|
|
||||||
message.error('An outbound with this tag already exists.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let result;
|
// If user spent time in the JSON tab, prefer that body — round-trip
|
||||||
try { result = buildResult(); } catch (_e) { return; }
|
// it through Outbound.fromJson so the wire shape stays consistent.
|
||||||
emit('confirm', result);
|
if (activeKey.value === '2' && advancedJson.value.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(advancedJson.value);
|
||||||
|
const built = Outbound.fromJson(parsed);
|
||||||
|
emit('confirm', built.toJson());
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
message.error(`JSON: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit('confirm', outbound.value.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = computed(() => (isEdit.value ? 'Edit outbound' : 'Add outbound'));
|
// ============== Link → outbound ==============
|
||||||
const okText = computed(() => (isEdit.value ? 'Update' : 'Add outbound'));
|
// The legacy "convert link" button takes a vmess://, vless://, ss://,
|
||||||
|
// trojan:// or hysteria2:// share-link string and rebuilds the
|
||||||
|
// outbound from it. The Outbound class doesn't have a native parser —
|
||||||
|
// we only support a friendly URL parse for the common shapes.
|
||||||
|
function convertLink() {
|
||||||
|
const link = linkInput.value.trim();
|
||||||
|
if (!link) return;
|
||||||
|
try {
|
||||||
|
if (link.startsWith('vmess://')) {
|
||||||
|
const data = JSON.parse(atob(link.replace(/^vmess:\/\//, '')));
|
||||||
|
const ob = new Outbound(data.ps || 'vmess', Protocols.VMess);
|
||||||
|
ob.settings.address = data.add;
|
||||||
|
ob.settings.port = Number(data.port) || 443;
|
||||||
|
ob.settings.id = data.id;
|
||||||
|
ob.settings.security = data.scy || USERS_SECURITY.AUTO;
|
||||||
|
ob.stream.network = data.net || 'tcp';
|
||||||
|
if (data.tls === 'tls') ob.stream.security = 'tls';
|
||||||
|
outbound.value = ob;
|
||||||
|
message.success(t('copySuccess'));
|
||||||
|
activeKey.value = '1';
|
||||||
|
} else {
|
||||||
|
message.warning('Only vmess:// links are supported by the quick converter for now — paste full JSON in the editor instead.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(`Link parse: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = computed(() =>
|
||||||
|
isEdit.value
|
||||||
|
? `${t('edit')} ${t('pages.xray.Outbounds')}`
|
||||||
|
: `+ ${t('pages.xray.Outbounds')}`,
|
||||||
|
);
|
||||||
|
const okText = computed(() =>
|
||||||
|
isEdit.value ? t('pages.client.submitEdit') : t('create'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper getters / shortcuts used by the template.
|
||||||
|
const proto = computed(() => outbound.value?.protocol);
|
||||||
|
const isVMess = computed(() => proto.value === Protocols.VMess);
|
||||||
|
const isVLESS = computed(() => proto.value === Protocols.VLESS);
|
||||||
|
const isVMessOrVLess = computed(() => isVMess.value || isVLESS.value);
|
||||||
|
const isTrojan = computed(() => proto.value === Protocols.Trojan);
|
||||||
|
const isShadowsocks = computed(() => proto.value === Protocols.Shadowsocks);
|
||||||
|
const isSocks = computed(() => proto.value === Protocols.Socks);
|
||||||
|
const isHTTP = computed(() => proto.value === Protocols.HTTP);
|
||||||
|
const isFreedom = computed(() => proto.value === Protocols.Freedom);
|
||||||
|
const isBlackhole = computed(() => proto.value === Protocols.Blackhole);
|
||||||
|
const isDNS = computed(() => proto.value === Protocols.DNS);
|
||||||
|
const isWireguard = computed(() => proto.value === Protocols.Wireguard);
|
||||||
|
const isHysteria = computed(() => proto.value === Protocols.Hysteria);
|
||||||
|
|
||||||
|
function regenerateWgKeys() {
|
||||||
|
if (!outbound.value?.settings) return;
|
||||||
|
const pair = Wireguard.generateKeypair();
|
||||||
|
outbound.value.settings.secretKey = pair.privateKey;
|
||||||
|
outbound.value.settings.pubKey = pair.publicKey;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal :open="open" :title="title" :ok-text="okText" :cancel-text="t('close')" :mask-closable="false" width="780px"
|
||||||
:open="open"
|
@ok="onOk" @cancel="close">
|
||||||
:title="title"
|
<a-tabs v-if="outbound" v-model:active-key="activeKey">
|
||||||
:ok-text="okText"
|
<!-- ============================== FORM ============================== -->
|
||||||
cancel-text="Close"
|
<a-tab-pane key="1" :tab="t('pages.xray.basicTemplate')">
|
||||||
:mask-closable="false"
|
|
||||||
width="720px"
|
|
||||||
@ok="onOk"
|
|
||||||
@cancel="close"
|
|
||||||
>
|
|
||||||
<a-tabs default-active-key="basic">
|
|
||||||
<a-tab-pane key="basic" tab="Basics">
|
|
||||||
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
||||||
<a-form-item label="Tag">
|
<!-- Protocol -->
|
||||||
<a-input v-model:value="form.tag" placeholder="unique-tag" />
|
<a-form-item :label="t('protocol')">
|
||||||
</a-form-item>
|
<a-select :value="proto" @change="onProtocolChange">
|
||||||
<a-form-item label="Protocol">
|
|
||||||
<a-select v-model:value="form.protocol">
|
|
||||||
<a-select-option v-for="p in PROTOCOL_OPTIONS" :key="p" :value="p">{{ p }}</a-select-option>
|
<a-select-option v-for="p in PROTOCOL_OPTIONS" :key="p" :value="p">{{ p }}</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- Tag -->
|
||||||
|
<a-form-item label="Tag" :validate-status="duplicateTag ? 'warning' : 'success'" has-feedback>
|
||||||
|
<a-input v-model:value="outbound.tag" placeholder="unique-tag" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- Send through -->
|
||||||
<a-form-item label="Send through">
|
<a-form-item label="Send through">
|
||||||
<a-input v-model:value="form.sendThrough" placeholder="local IP to bind to (optional)" />
|
<a-input v-model:value="outbound.sendThrough" placeholder="local IP" />
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="Domain strategy">
|
|
||||||
<a-select v-model:value="form.domainStrategy">
|
|
||||||
<a-select-option v-for="s in OutboundDomainStrategies" :key="s" :value="s">{{ s }}</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- ============== Freedom ============== -->
|
||||||
|
<template v-if="isFreedom">
|
||||||
|
<a-form-item label="Strategy">
|
||||||
|
<a-select v-model:value="outbound.settings.domainStrategy">
|
||||||
|
<a-select-option v-for="s in OutboundDomainStrategies" :key="s" :value="s">{{ s }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Redirect">
|
||||||
|
<a-input v-model:value="outbound.settings.redirect" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">Fragment</a-divider>
|
||||||
|
<a-form-item label="Fragment">
|
||||||
|
<a-switch :checked="!!outbound.settings.fragment && Object.keys(outbound.settings.fragment).length > 0"
|
||||||
|
@change="(checked) => outbound.settings.fragment = checked ? { packets: 'tlshello', length: '100-200', interval: '10-20', maxSplit: '300-400' } : {}" />
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="outbound.settings.fragment && Object.keys(outbound.settings.fragment).length > 0">
|
||||||
|
<a-form-item label="Packets">
|
||||||
|
<a-select v-model:value="outbound.settings.fragment.packets">
|
||||||
|
<a-select-option v-for="p in ['1-3', 'tlshello']" :key="p" :value="p">{{ p }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Length">
|
||||||
|
<a-input v-model:value="outbound.settings.fragment.length" placeholder="100-200" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Interval">
|
||||||
|
<a-input v-model:value="outbound.settings.fragment.interval" placeholder="10-20" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Max Split">
|
||||||
|
<a-input v-model:value="outbound.settings.fragment.maxSplit" placeholder="300-400" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">Noises</a-divider>
|
||||||
|
<a-form-item label="Noises">
|
||||||
|
<a-switch :checked="(outbound.settings.noises || []).length > 0"
|
||||||
|
@change="(checked) => outbound.settings.noises = checked ? [{ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ip' }] : []" />
|
||||||
|
<a-button v-if="outbound.settings.noises && outbound.settings.noises.length > 0" size="small"
|
||||||
|
type="primary" class="ml-8"
|
||||||
|
@click="outbound.settings.noises.push({ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ip' })">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
<template v-for="(noise, index) in outbound.settings.noises || []" :key="index">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">
|
||||||
|
Noise {{ index + 1 }}
|
||||||
|
<DeleteOutlined v-if="outbound.settings.noises.length > 1" class="danger-icon"
|
||||||
|
@click="outbound.settings.noises.splice(index, 1)" />
|
||||||
|
</a-divider>
|
||||||
|
<a-form-item label="Type">
|
||||||
|
<a-select v-model:value="noise.type">
|
||||||
|
<a-select-option v-for="x in ['rand', 'base64', 'str', 'hex']" :key="x" :value="x">{{ x
|
||||||
|
}}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Packet">
|
||||||
|
<a-input v-model:value="noise.packet" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Delay (ms)">
|
||||||
|
<a-input v-model:value="noise.delay" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Apply to">
|
||||||
|
<a-select v-model:value="noise.applyTo">
|
||||||
|
<a-select-option v-for="x in ['ip', 'ipv4', 'ipv6']" :key="x" :value="x">{{ x }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== Blackhole ============== -->
|
||||||
|
<template v-if="isBlackhole">
|
||||||
|
<a-form-item label="Response Type">
|
||||||
|
<a-select v-model:value="outbound.settings.type">
|
||||||
|
<a-select-option v-for="x in ['', 'none', 'http']" :key="x" :value="x">{{ x || '(empty)'
|
||||||
|
}}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== DNS ============== -->
|
||||||
|
<template v-if="isDNS">
|
||||||
|
<a-form-item :label="t('pages.inbounds.network')">
|
||||||
|
<a-select v-model:value="outbound.settings.network">
|
||||||
|
<a-select-option v-for="x in ['udp', 'tcp']" :key="x" :value="x">{{ x }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Rules">
|
||||||
|
<a-button size="small" type="primary"
|
||||||
|
@click="outbound.settings.rules.push({ action: 'direct', qtype: '', domain: '' })">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
<template v-for="(rule, index) in outbound.settings.rules || []" :key="index">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">
|
||||||
|
Rule {{ index + 1 }}
|
||||||
|
<DeleteOutlined class="danger-icon" @click="outbound.settings.rules.splice(index, 1)" />
|
||||||
|
</a-divider>
|
||||||
|
<a-form-item label="Action">
|
||||||
|
<a-select v-model:value="rule.action">
|
||||||
|
<a-select-option v-for="a in DNSRuleActions" :key="a" :value="a">{{ a }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="QType">
|
||||||
|
<a-input v-model:value="rule.qtype" placeholder="1,3,23-24" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('domainName')">
|
||||||
|
<a-input v-model:value="rule.domain" placeholder="domain:example.com" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== WireGuard ============== -->
|
||||||
|
<template v-if="isWireguard">
|
||||||
|
<a-form-item :label="t('pages.inbounds.address')">
|
||||||
|
<a-input v-model:value="outbound.settings.address" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<template #label>
|
||||||
|
{{ t('pages.inbounds.privatekey') }}
|
||||||
|
<SyncOutlined class="random-icon" @click="regenerateWgKeys" />
|
||||||
|
</template>
|
||||||
|
<a-input v-model:value="outbound.settings.secretKey" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||||
|
<a-input :value="outbound.settings.pubKey" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Domain strategy">
|
||||||
|
<a-select v-model:value="outbound.settings.domainStrategy">
|
||||||
|
<a-select-option v-for="x in ['', ...WireguardDomainStrategy]" :key="x || '__'" :value="x">
|
||||||
|
{{ x || `(${t('none')})` }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="MTU">
|
||||||
|
<a-input-number v-model:value="outbound.settings.mtu" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Workers">
|
||||||
|
<a-input-number v-model:value="outbound.settings.workers" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="No-kernel TUN">
|
||||||
|
<a-switch v-model:checked="outbound.settings.noKernelTun" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Reserved">
|
||||||
|
<a-input v-model:value="outbound.settings.reserved" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Peers">
|
||||||
|
<a-button size="small" type="primary"
|
||||||
|
@click="outbound.settings.peers.push({ endpoint: '', publicKey: '', psk: '', allowedIPs: [''], keepAlive: 0 })">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
<template v-for="(peer, index) in outbound.settings.peers || []" :key="index">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">
|
||||||
|
Peer {{ index + 1 }}
|
||||||
|
<DeleteOutlined v-if="outbound.settings.peers.length > 1" class="danger-icon"
|
||||||
|
@click="outbound.settings.peers.splice(index, 1)" />
|
||||||
|
</a-divider>
|
||||||
|
<a-form-item label="Endpoint">
|
||||||
|
<a-input v-model:value="peer.endpoint" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||||
|
<a-input v-model:value="peer.publicKey" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="PSK">
|
||||||
|
<a-input v-model:value="peer.psk" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Allowed IPs">
|
||||||
|
<template v-for="(_, idx) in peer.allowedIPs" :key="idx">
|
||||||
|
<a-input v-model:value="peer.allowedIPs[idx]" :style="{ marginBottom: '4px' }">
|
||||||
|
<template v-if="peer.allowedIPs.length > 1" #addonAfter>
|
||||||
|
<MinusOutlined @click="peer.allowedIPs.splice(idx, 1)" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</template>
|
||||||
|
<a-button size="small" @click="peer.allowedIPs.push('')">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Keep alive">
|
||||||
|
<a-input-number v-model:value="peer.keepAlive" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== Address + Port (most protocols) ============== -->
|
||||||
|
<template v-if="outbound.hasAddressPort()">
|
||||||
|
<a-form-item :label="t('pages.inbounds.address')">
|
||||||
|
<a-input v-model:value="outbound.settings.address" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.inbounds.port')">
|
||||||
|
<a-input-number v-model:value="outbound.settings.port" :min="1" :max="65535" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== VMess / VLess user ============== -->
|
||||||
|
<template v-if="isVMessOrVLess">
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input v-model:value="outbound.settings.id" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="isVMess" :label="t('security')">
|
||||||
|
<a-select v-model:value="outbound.settings.security">
|
||||||
|
<a-select-option v-for="s in SECURITY_OPTIONS" :key="s" :value="s">{{ s }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="isVLESS" :label="t('encryption')">
|
||||||
|
<a-input v-model:value="outbound.settings.encryption" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="isVLESS" label="Reverse tag">
|
||||||
|
<a-input v-model:value="outbound.settings.reverseTag" placeholder="optional" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="outbound.canEnableTlsFlow()" label="Flow">
|
||||||
|
<a-select v-model:value="outbound.settings.flow">
|
||||||
|
<a-select-option value="">{{ t('none') }}</a-select-option>
|
||||||
|
<a-select-option v-for="key in FLOW_OPTIONS" :key="key" :value="key">{{ key }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== Trojan / Shadowsocks ============== -->
|
||||||
|
<template v-if="isTrojan || isShadowsocks">
|
||||||
|
<a-form-item :label="t('password')">
|
||||||
|
<a-input v-model:value="outbound.settings.password" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-if="isShadowsocks">
|
||||||
|
<a-form-item :label="t('encryption')">
|
||||||
|
<a-select v-model:value="outbound.settings.method">
|
||||||
|
<a-select-option v-for="(m, k) in SSMethods" :key="m" :value="m">{{ k }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="UDP over TCP">
|
||||||
|
<a-switch v-model:checked="outbound.settings.uot" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="UoT version">
|
||||||
|
<a-input-number v-model:value="outbound.settings.UoTVersion" :min="1" :max="2" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== SOCKS / HTTP ============== -->
|
||||||
|
<template v-if="outbound.hasUsername()">
|
||||||
|
<a-form-item :label="t('username')">
|
||||||
|
<a-input v-model:value="outbound.settings.user" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('password')">
|
||||||
|
<a-input v-model:value="outbound.settings.pass" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== Hysteria ============== -->
|
||||||
|
<template v-if="isHysteria">
|
||||||
|
<a-form-item label="Version">
|
||||||
|
<a-input-number :value="outbound.settings.version || 2" :min="2" :max="2" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== Stream settings ============== -->
|
||||||
|
<template v-if="outbound.canEnableStream()">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">{{ t('transmission') }}</a-divider>
|
||||||
|
<a-form-item :label="t('transmission')">
|
||||||
|
<a-select :value="outbound.stream.network" @change="streamNetworkChange">
|
||||||
|
<a-select-option v-for="net in (isHysteria ? [...NETWORKS, 'hysteria'] : NETWORKS)" :key="net"
|
||||||
|
:value="net">
|
||||||
|
{{ NETWORK_LABELS[net] || net }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- TCP -->
|
||||||
|
<template v-if="outbound.stream.network === 'tcp'">
|
||||||
|
<a-form-item :label="`HTTP ${t('camouflage')}`">
|
||||||
|
<a-switch :checked="outbound.stream.tcp.type === 'http'"
|
||||||
|
@change="(checked) => outbound.stream.tcp.type = checked ? 'http' : 'none'" />
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="outbound.stream.tcp.type === 'http'">
|
||||||
|
<a-form-item :label="t('host')">
|
||||||
|
<a-input v-model:value="outbound.stream.tcp.host" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('path')">
|
||||||
|
<a-input v-model:value="outbound.stream.tcp.path" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- KCP -->
|
||||||
|
<template v-if="outbound.stream.network === 'kcp'">
|
||||||
|
<a-form-item label="MTU">
|
||||||
|
<a-input-number v-model:value="outbound.stream.kcp.mtu" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="TTI (ms)">
|
||||||
|
<a-input-number v-model:value="outbound.stream.kcp.tti" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Uplink (MB/s)">
|
||||||
|
<a-input-number v-model:value="outbound.stream.kcp.upCap" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Downlink (MB/s)">
|
||||||
|
<a-input-number v-model:value="outbound.stream.kcp.downCap" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="CWND multiplier">
|
||||||
|
<a-input-number v-model:value="outbound.stream.kcp.cwndMultiplier" :min="1" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Max sending window">
|
||||||
|
<a-input-number v-model:value="outbound.stream.kcp.maxSendingWindow" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- WebSocket -->
|
||||||
|
<template v-if="outbound.stream.network === 'ws'">
|
||||||
|
<a-form-item :label="t('host')">
|
||||||
|
<a-input v-model:value="outbound.stream.ws.host" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('path')">
|
||||||
|
<a-input v-model:value="outbound.stream.ws.path" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Heartbeat (s)">
|
||||||
|
<a-input-number v-model:value="outbound.stream.ws.heartbeatPeriod" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- gRPC -->
|
||||||
|
<template v-if="outbound.stream.network === 'grpc'">
|
||||||
|
<a-form-item label="Service name">
|
||||||
|
<a-input v-model:value="outbound.stream.grpc.serviceName" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Authority">
|
||||||
|
<a-input v-model:value="outbound.stream.grpc.authority" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Multi mode">
|
||||||
|
<a-switch v-model:checked="outbound.stream.grpc.multiMode" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- HTTPUpgrade -->
|
||||||
|
<template v-if="outbound.stream.network === 'httpupgrade'">
|
||||||
|
<a-form-item :label="t('host')">
|
||||||
|
<a-input v-model:value="outbound.stream.httpupgrade.host" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('path')">
|
||||||
|
<a-input v-model:value="outbound.stream.httpupgrade.path" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- XHTTP -->
|
||||||
|
<template v-if="outbound.stream.network === 'xhttp'">
|
||||||
|
<a-form-item :label="t('host')">
|
||||||
|
<a-input v-model:value="outbound.stream.xhttp.host" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('path')">
|
||||||
|
<a-input v-model:value="outbound.stream.xhttp.path" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Mode">
|
||||||
|
<a-select v-model:value="outbound.stream.xhttp.mode">
|
||||||
|
<a-select-option v-for="m in Object.values(MODE_OPTION)" :key="m" :value="m">{{ m }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Padding bytes">
|
||||||
|
<a-input v-model:value="outbound.stream.xhttp.xPaddingBytes" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="XMUX">
|
||||||
|
<a-switch v-model:checked="outbound.stream.xhttp.enableXmux" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Hysteria transport -->
|
||||||
|
<template v-if="outbound.stream.network === 'hysteria'">
|
||||||
|
<a-form-item label="Auth password">
|
||||||
|
<a-input v-model:value="outbound.stream.hysteria.auth" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Congestion">
|
||||||
|
<a-select v-model:value="outbound.stream.hysteria.congestion">
|
||||||
|
<a-select-option value="">BBR (auto)</a-select-option>
|
||||||
|
<a-select-option value="brutal">Brutal</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Upload">
|
||||||
|
<a-input v-model:value="outbound.stream.hysteria.up" placeholder="100 mbps" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Download">
|
||||||
|
<a-input v-model:value="outbound.stream.hysteria.down" placeholder="100 mbps" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="UDP hop port">
|
||||||
|
<a-input v-model:value="outbound.stream.hysteria.udphopPort" placeholder="1145-1919" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Max idle (s)">
|
||||||
|
<a-input-number v-model:value="outbound.stream.hysteria.maxIdleTimeout" :min="4" :max="120" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Keep alive (s)">
|
||||||
|
<a-input-number v-model:value="outbound.stream.hysteria.keepAlivePeriod" :min="2" :max="60" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Disable Path MTU">
|
||||||
|
<a-switch v-model:checked="outbound.stream.hysteria.disablePathMTUDiscovery" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== TLS / Reality ============== -->
|
||||||
|
<template v-if="outbound.canEnableTls()">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">{{ t('security') }}</a-divider>
|
||||||
|
<a-form-item :label="t('security')">
|
||||||
|
<a-radio-group v-model:value="outbound.stream.security" button-style="solid">
|
||||||
|
<a-radio-button value="none">{{ t('none') }}</a-radio-button>
|
||||||
|
<a-radio-button value="tls">TLS</a-radio-button>
|
||||||
|
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<template v-if="outbound.stream.isTls">
|
||||||
|
<a-form-item label="SNI">
|
||||||
|
<a-input v-model:value="outbound.stream.tls.serverName" placeholder="server name" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="uTLS">
|
||||||
|
<a-select v-model:value="outbound.stream.tls.fingerprint">
|
||||||
|
<a-select-option value="">{{ t('none') }}</a-select-option>
|
||||||
|
<a-select-option v-for="key in UTLS_OPTIONS" :key="key" :value="key">{{ key }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="ALPN">
|
||||||
|
<a-select v-model:value="outbound.stream.tls.alpn" mode="multiple">
|
||||||
|
<a-select-option v-for="alpn in ALPN_OPTIONS" :key="alpn" :value="alpn">{{ alpn }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="ECH">
|
||||||
|
<a-input v-model:value="outbound.stream.tls.echConfigList" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Verify peer name">
|
||||||
|
<a-input v-model:value="outbound.stream.tls.verifyPeerCertByName" placeholder="cloudflare-dns.com" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Pinned SHA256">
|
||||||
|
<a-input v-model:value="outbound.stream.tls.pinnedPeerCertSha256" placeholder="base64 SHA256" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="outbound.stream.isReality">
|
||||||
|
<a-form-item label="SNI">
|
||||||
|
<a-input v-model:value="outbound.stream.reality.serverName" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="uTLS">
|
||||||
|
<a-select v-model:value="outbound.stream.reality.fingerprint">
|
||||||
|
<a-select-option v-for="key in UTLS_OPTIONS" :key="key" :value="key">{{ key }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Short ID">
|
||||||
|
<a-input v-model:value="outbound.stream.reality.shortId" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="SpiderX">
|
||||||
|
<a-input v-model:value="outbound.stream.reality.spiderX" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||||
|
<a-textarea v-model:value="outbound.stream.reality.publicKey" :auto-size="{ minRows: 2 }" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="mldsa65 verify">
|
||||||
|
<a-textarea v-model:value="outbound.stream.reality.mldsa65Verify" :auto-size="{ minRows: 2 }" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== sockopt ============== -->
|
||||||
|
<template v-if="outbound.stream">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">Sockopts</a-divider>
|
||||||
|
<a-form-item label="Sockopts">
|
||||||
|
<a-switch v-model:checked="outbound.stream.sockoptSwitch" />
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="outbound.stream.sockoptSwitch">
|
||||||
|
<a-form-item label="Dialer proxy">
|
||||||
|
<a-input v-model:value="outbound.stream.sockopt.dialerProxy" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Address+Port strategy">
|
||||||
|
<a-select v-model:value="outbound.stream.sockopt.addressPortStrategy">
|
||||||
|
<a-select-option v-for="key in Object.values(Address_Port_Strategy)" :key="key" :value="key">
|
||||||
|
{{ key }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Keep alive interval">
|
||||||
|
<a-input-number v-model:value="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="TCP Fast Open">
|
||||||
|
<a-switch v-model:checked="outbound.stream.sockopt.tcpFastOpen" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Multipath TCP">
|
||||||
|
<a-switch v-model:checked="outbound.stream.sockopt.tcpMptcp" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Penetrate">
|
||||||
|
<a-switch v-model:checked="outbound.stream.sockopt.penetrate" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== Mux ============== -->
|
||||||
|
<template v-if="outbound.canEnableMux()">
|
||||||
|
<a-divider :style="{ margin: '4px 0' }">{{ t('pages.settings.mux') }}</a-divider>
|
||||||
|
<a-form-item :label="t('pages.settings.mux')">
|
||||||
|
<a-switch v-model:checked="outbound.mux.enabled" />
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="outbound.mux.enabled">
|
||||||
|
<a-form-item label="Concurrency">
|
||||||
|
<a-input-number v-model:value="outbound.mux.concurrency" :min="-1" :max="1024" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="xudp concurrency">
|
||||||
|
<a-input-number v-model:value="outbound.mux.xudpConcurrency" :min="-1" :max="1024" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="xudp UDP 443">
|
||||||
|
<a-select v-model:value="outbound.mux.xudpProxyUDP443">
|
||||||
|
<a-select-option v-for="x in ['reject', 'allow', 'skip']" :key="x" :value="x">{{ x
|
||||||
|
}}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<a-tab-pane key="settings" tab="settings (JSON)">
|
<!-- ============================== JSON ============================== -->
|
||||||
<a-alert
|
<a-tab-pane key="2" tab="JSON">
|
||||||
type="info"
|
<a-space direction="vertical" :size="10" :style="{ width: '100%', marginTop: '10px' }">
|
||||||
show-icon
|
<a-input-search v-model:value="linkInput" placeholder="vmess:// vless:// trojan:// ss:// hysteria2://"
|
||||||
message="Edit the protocol-specific settings tree directly. Leave empty to omit."
|
@search="convertLink">
|
||||||
class="mb-12"
|
<template #enterButton>
|
||||||
/>
|
<a-button>Convert</a-button>
|
||||||
<a-textarea
|
</template>
|
||||||
v-model:value="form.settingsText"
|
</a-input-search>
|
||||||
:auto-size="{ minRows: 12, maxRows: 28 }"
|
<a-textarea v-model:value="advancedJson" :auto-size="{ minRows: 14, maxRows: 30 }" spellcheck="false"
|
||||||
spellcheck="false"
|
class="json-editor" />
|
||||||
class="json-editor"
|
</a-space>
|
||||||
/>
|
|
||||||
</a-tab-pane>
|
|
||||||
|
|
||||||
<a-tab-pane key="stream" tab="streamSettings (JSON)">
|
|
||||||
<a-alert
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
message="Transport / TLS / Reality / mux options. Leave empty to omit."
|
|
||||||
class="mb-12"
|
|
||||||
/>
|
|
||||||
<a-textarea
|
|
||||||
v-model:value="form.streamSettingsText"
|
|
||||||
:auto-size="{ minRows: 12, maxRows: 28 }"
|
|
||||||
spellcheck="false"
|
|
||||||
class="json-editor"
|
|
||||||
/>
|
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mb-12 { margin-bottom: 12px; }
|
.random-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--ant-primary-color, #1890ff);
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-8 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.json-editor {
|
.json-editor {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue