3x-ui/frontend/src/pages/xray/BalancerFormModal.vue

112 lines
3.4 KiB
Vue
Raw Normal View History

feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:30:48 +00:00
<script setup>
import { computed, reactive, ref, watch } from 'vue';
// Balancer add/edit modal — mirrors xray_balancer_modal.html.
// Tag must be unique across other balancers; selector is a tag-mode
// list constrained to existing outbound tags (but lets users type
// new ones for forward-references).
const props = defineProps({
open: { type: Boolean, default: false },
balancer: { type: Object, default: null },
outboundTags: { type: Array, default: () => [] },
// All other balancer tags (excludes the one currently being edited)
// — used for the duplicate-tag check.
otherTags: { type: Array, default: () => [] },
});
const emit = defineEmits(['update:open', 'confirm']);
const STRATEGIES = [
{ value: 'random', label: 'Random' },
{ value: 'roundRobin', label: 'Round robin' },
{ value: 'leastLoad', label: 'Least load' },
{ value: 'leastPing', label: 'Least ping' },
];
const form = reactive({
tag: '',
strategy: 'random',
selector: [],
fallbackTag: '',
});
const isEdit = ref(false);
watch(() => props.open, (next) => {
if (!next) return;
if (props.balancer) {
isEdit.value = true;
form.tag = props.balancer.tag || '';
form.strategy = props.balancer.strategy || 'random';
form.selector = [...(props.balancer.selector || [])];
form.fallbackTag = props.balancer.fallbackTag || '';
} else {
isEdit.value = false;
form.tag = '';
form.strategy = 'random';
form.selector = [];
form.fallbackTag = '';
}
});
const duplicateTag = computed(
() => !form.tag || props.otherTags.includes(form.tag),
);
const emptySelector = computed(() => form.selector.length === 0);
const isValid = computed(() => !duplicateTag.value && !emptySelector.value);
function close() { emit('update:open', false); }
function onOk() {
if (!isValid.value) return;
emit('confirm', { ...form });
}
const title = computed(() => (isEdit.value ? 'Edit balancer' : 'Add balancer'));
const okText = computed(() => (isEdit.value ? 'Update' : 'Add'));
</script>
<template>
<a-modal
:open="open"
:title="title"
:ok-text="okText"
cancel-text="Close"
:ok-button-props="{ disabled: !isValid }"
:mask-closable="false"
@ok="onOk"
@cancel="close"
>
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
<a-form-item
label="Tag"
:validate-status="duplicateTag ? 'warning' : 'success'"
has-feedback
>
<a-input v-model:value="form.tag" placeholder="unique balancer tag" />
</a-form-item>
<a-form-item label="Strategy">
<a-select v-model:value="form.strategy">
<a-select-option v-for="s in STRATEGIES" :key="s.value" :value="s.value">{{ s.label }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="Selector"
:validate-status="emptySelector ? 'warning' : 'success'"
has-feedback
>
<a-select v-model:value="form.selector" mode="tags" :token-separators="[',']">
<a-select-option v-for="t in outboundTags" :key="t" :value="t">{{ t }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Fallback">
<a-select v-model:value="form.fallbackTag" allow-clear>
<a-select-option v-for="t in ['', ...outboundTags]" :key="t || '__empty'" :value="t">{{ t || '(none)' }}</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>