web/frpc: redesign frpc dashboard with sidebar nav, proxy/visitor list and detail views (#5237)

This commit is contained in:
fatedier
2026-03-16 09:44:30 +08:00
committed by GitHub
parent ff4ad2f907
commit 85e8e2c830
71 changed files with 5908 additions and 4292 deletions

View File

@@ -0,0 +1,40 @@
<template>
<div v-if="!readonly" class="field-row three-col">
<el-form-item label="Name" prop="name" class="field-grow">
<el-input v-model="form.name" :disabled="editing || readonly" placeholder="my-visitor" />
</el-form-item>
<ConfigField label="Type" type="select" v-model="form.type" :disabled="editing"
:options="[{ label: 'STCP', value: 'stcp' }, { label: 'SUDP', value: 'sudp' }, { label: 'XTCP', value: 'xtcp' }]" prop="type" />
<el-form-item label="Enabled" class="switch-field">
<el-switch v-model="form.enabled" size="small" />
</el-form-item>
</div>
<div v-else class="field-row three-col">
<ConfigField label="Name" type="text" :model-value="form.name" readonly class="field-grow" />
<ConfigField label="Type" type="text" :model-value="form.type.toUpperCase()" readonly />
<ConfigField label="Enabled" type="switch" :model-value="form.enabled" readonly />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VisitorFormData } from '../../types'
import ConfigField from '../ConfigField.vue'
const props = withDefaults(defineProps<{
modelValue: VisitorFormData
readonly?: boolean
editing?: boolean
}>(), { readonly: false, editing: false })
const emit = defineEmits<{ 'update:modelValue': [value: VisitorFormData] }>()
const form = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>
<style scoped lang="scss">
@use '@/assets/css/form-layout';
</style>

View File

@@ -0,0 +1,43 @@
<template>
<ConfigSection title="Connection" :readonly="readonly">
<div class="field-row two-col">
<ConfigField label="Server Name" type="text" v-model="form.serverName" prop="serverName"
placeholder="Name of the proxy to visit" :readonly="readonly" />
<ConfigField label="Server User" type="text" v-model="form.serverUser"
placeholder="Leave empty for same user" :readonly="readonly" />
</div>
<ConfigField label="Secret Key" type="password" v-model="form.secretKey"
placeholder="Shared secret" :readonly="readonly" />
<div class="field-row two-col">
<ConfigField label="Bind Address" type="text" v-model="form.bindAddr"
placeholder="127.0.0.1" :readonly="readonly" />
<ConfigField label="Bind Port" type="number" v-model="form.bindPort"
:min="bindPortMin" :max="65535" prop="bindPort" :readonly="readonly" />
</div>
</ConfigSection>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VisitorFormData } from '../../types'
import ConfigSection from '../ConfigSection.vue'
import ConfigField from '../ConfigField.vue'
const props = withDefaults(defineProps<{
modelValue: VisitorFormData
readonly?: boolean
}>(), { readonly: false })
const emit = defineEmits<{ 'update:modelValue': [value: VisitorFormData] }>()
const form = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const bindPortMin = computed(() => (form.value.type === 'sudp' ? 1 : undefined))
</script>
<style scoped lang="scss">
@use '@/assets/css/form-layout';
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="visitor-form-layout">
<ConfigSection :readonly="readonly">
<VisitorBaseSection v-model="form" :readonly="readonly" :editing="editing" />
</ConfigSection>
<VisitorConnectionSection v-model="form" :readonly="readonly" />
<VisitorTransportSection v-model="form" :readonly="readonly" />
<VisitorXtcpSection v-if="form.type === 'xtcp'" v-model="form" :readonly="readonly" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VisitorFormData } from '../../types'
import ConfigSection from '../ConfigSection.vue'
import VisitorBaseSection from './VisitorBaseSection.vue'
import VisitorConnectionSection from './VisitorConnectionSection.vue'
import VisitorTransportSection from './VisitorTransportSection.vue'
import VisitorXtcpSection from './VisitorXtcpSection.vue'
const props = withDefaults(defineProps<{
modelValue: VisitorFormData
readonly?: boolean
editing?: boolean
}>(), { readonly: false, editing: false })
const emit = defineEmits<{ 'update:modelValue': [value: VisitorFormData] }>()
const form = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>

View File

@@ -0,0 +1,32 @@
<template>
<ConfigSection title="Transport Options" collapsible :readonly="readonly"
:has-value="form.useEncryption || form.useCompression">
<div class="field-row two-col">
<ConfigField label="Use Encryption" type="switch" v-model="form.useEncryption" :readonly="readonly" />
<ConfigField label="Use Compression" type="switch" v-model="form.useCompression" :readonly="readonly" />
</div>
</ConfigSection>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VisitorFormData } from '../../types'
import ConfigSection from '../ConfigSection.vue'
import ConfigField from '../ConfigField.vue'
const props = withDefaults(defineProps<{
modelValue: VisitorFormData
readonly?: boolean
}>(), { readonly: false })
const emit = defineEmits<{ 'update:modelValue': [value: VisitorFormData] }>()
const form = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>
<style scoped lang="scss">
@use '@/assets/css/form-layout';
</style>

View File

@@ -0,0 +1,47 @@
<template>
<!-- XTCP Options -->
<ConfigSection title="XTCP Options" collapsible :readonly="readonly"
:has-value="form.protocol !== 'quic' || form.keepTunnelOpen || form.maxRetriesAnHour != null || form.minRetryInterval != null || !!form.fallbackTo || form.fallbackTimeoutMs != null">
<ConfigField label="Protocol" type="select" v-model="form.protocol"
:options="[{ label: 'QUIC', value: 'quic' }, { label: 'KCP', value: 'kcp' }]" :readonly="readonly" />
<ConfigField label="Keep Tunnel Open" type="switch" v-model="form.keepTunnelOpen" :readonly="readonly" />
<div class="field-row two-col">
<ConfigField label="Max Retries per Hour" type="number" v-model="form.maxRetriesAnHour" :min="0" :readonly="readonly" />
<ConfigField label="Min Retry Interval (s)" type="number" v-model="form.minRetryInterval" :min="0" :readonly="readonly" />
</div>
<div class="field-row two-col">
<ConfigField label="Fallback To" type="text" v-model="form.fallbackTo" placeholder="Fallback visitor name" :readonly="readonly" />
<ConfigField label="Fallback Timeout (ms)" type="number" v-model="form.fallbackTimeoutMs" :min="0" :readonly="readonly" />
</div>
</ConfigSection>
<!-- NAT Traversal -->
<ConfigSection title="NAT Traversal" collapsible :readonly="readonly"
:has-value="form.natTraversalDisableAssistedAddrs">
<ConfigField label="Disable Assisted Addresses" type="switch" v-model="form.natTraversalDisableAssistedAddrs"
tip="Only use STUN-discovered public addresses" :readonly="readonly" />
</ConfigSection>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VisitorFormData } from '../../types'
import ConfigSection from '../ConfigSection.vue'
import ConfigField from '../ConfigField.vue'
const props = withDefaults(defineProps<{
modelValue: VisitorFormData
readonly?: boolean
}>(), { readonly: false })
const emit = defineEmits<{ 'update:modelValue': [value: VisitorFormData] }>()
const form = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>
<style scoped lang="scss">
@use '@/assets/css/form-layout';
</style>