add
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<v-overlay
|
||||
:model-value="loading"
|
||||
persistent
|
||||
content-class="text-center"
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="64"
|
||||
></v-progress-circular>
|
||||
<br />
|
||||
{{ $t('loading') }}
|
||||
</v-overlay>
|
||||
<Message />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Message from '@/components/message.vue'
|
||||
import { inject, ref, Ref } from 'vue'
|
||||
|
||||
const loading:Ref = inject('loading')?? ref(false)
|
||||
|
||||
// Change page title
|
||||
document.title = "S-UI " + document.location.hostname
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-overlay .v-list-item,
|
||||
.v-field__input {
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 763 B |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="1.019 0.0225 45.9789 46.9775" width="45.9789" height="46.9775" xmlns="http://www.w3.org/2000/svg">
|
||||
<g featurekey="symbolFeature-0" transform="matrix(0.4545450210571289, 0, 0, 0.4545450210571289, 0.7917079329490662, 1.7009549140930176)" fill="#737373">
|
||||
<g xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M50,99.658L0.5,70.699V29.301L50,0.341l49.5,28.959v41.398L50,99.658z M2.5,69.553L50,97.342l47.5-27.789V30.448L50,2.659 L2.5,30.448V69.553z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon points="51,98.376 49,98.376 49,58.822 0.995,30.738 2.005,29.011 50,57.091 97.995,29.011 99.005,30.738 51,58.822 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline points="28.494,14.082 76.994,42.457 71.506,45.667 23.006,17.292 "/>
|
||||
<polygon points="71.507,46.246 71.254,46.098 22.754,17.724 23.259,16.861 71.507,45.087 76.003,42.457 28.241,14.514 28.746,13.65 77.983,42.457 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline points="71.506,45.667 71.506,57.982 71.51,57.982 76.993,54.775 76.993,42.457 "/>
|
||||
<polyline points="71.006,45.667 72.006,45.667 72.006,57.113 76.493,54.487 76.493,42.457 77.493,42.457 77.493,55.062 71.646,58.482 71,58.85 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g featurekey="nameFeature-0" transform="matrix(1.6160469055175781, 0, 0, 1.6160469055175781, 3.2854819297790527, -21.369783401489258)" fill="#a6a6a6">
|
||||
<path d="M10.904 40.4028 c-5.316 0 -9.2256 -3.048 -9.2256 -6.6192 c0 -1.8592 1.2372 -2.8152 2.5116 -2.8152 c1.0924 0 2.2288 0.7184 2.2288 2.1488 c0 1.4428 -1.4112 1.9972 -1.4112 2.9972 c0 1.782 2.974 3.0552 5.338 3.0552 c3.244 0 6.382 -1.5736 6.382 -5.102 c0 -6.2716 -14.403 -3.6536 -14.403 -13.229 c0 -5.0924 4.3436 -7.6012 9.9036 -7.6012 c4.7364 0 8.9656 2.6496 8.9656 6.1048 c0 1.946 -1.2372 2.9308 -2.4828 2.9308 c-1.1216 0 -2.258 -0.7476 -2.258 -2.178 c0 -1.5584 1.382 -1.8812 1.382 -2.8812 c0 -1.6952 -2.974 -2.7436 -5.3092 -2.7436 c-3.04 0 -5.9296 1.1848 -5.9296 4.6496 c0 5.866 15.193 3.7708 15.193 13.345 c0 4.0208 -3.8304 7.938 -10.886 7.938 z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
required
|
||||
v-model="addr.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
required
|
||||
v-model.number="addr.server_port"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionRemark">
|
||||
<v-text-field
|
||||
:label="$t('in.remark')"
|
||||
hide-details
|
||||
v-model="addr.remark">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<OutTLS :outbound="addr" v-if="optionTLS" />
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto" align="end" justify="center">
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('in.mdOption') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRemark" color="primary" :label="$t('in.remark')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="hasTls">
|
||||
<v-switch v-model="optionTLS" color="primary" :label="$t('objects.tls')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import OutTLS from '@/components/tls/OutTLS.vue'
|
||||
export default {
|
||||
props: ['addr', 'hasTls'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionTLS: {
|
||||
get(): boolean { return this.$props.addr.tls != undefined },
|
||||
set(v:boolean) { this.$props.addr.tls = v ? { enabled: true } : undefined }
|
||||
},
|
||||
optionRemark: {
|
||||
get(): boolean { return this.$props.addr.remark != undefined },
|
||||
set(v:boolean) { this.$props.addr.remark = v ? '' : undefined }
|
||||
}
|
||||
},
|
||||
components: {
|
||||
OutTLS
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
id="expiry"
|
||||
:label="$t('date.expiry')"
|
||||
v-model="dateFormatted"
|
||||
prepend-inner-icon="mdi-calendar"
|
||||
readonly
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<DatePicker
|
||||
v-model="Input"
|
||||
@input="Input=$event"
|
||||
:locale="locale"
|
||||
element="expiry"
|
||||
compact-time
|
||||
type="datetime">
|
||||
<template v-slot:next-month>
|
||||
<v-icon icon="mdi-chevron-right" />
|
||||
</template>
|
||||
<template v-slot:prev-month>
|
||||
<v-icon icon="mdi-chevron-left" />
|
||||
</template>
|
||||
<template #submit-btn="{ submit, canSubmit }">
|
||||
<v-btn
|
||||
:disabled="!canSubmit"
|
||||
@click="submit"
|
||||
>{{ $t('submit') }}</v-btn>
|
||||
</template>
|
||||
<template #cancel-btn="{ vm }">
|
||||
<v-btn
|
||||
@click="reset(vm)"
|
||||
>{{ $t('reset') }}</v-btn>
|
||||
</template>
|
||||
<template #now-btn="{ goToday }">
|
||||
<v-btn
|
||||
@click="goToday"
|
||||
>{{ $t('now') }}</v-btn>
|
||||
</template>
|
||||
</DatePicker>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DatePicker from 'vue3-persian-datetime-picker'
|
||||
import { i18n, locale } from '@/locales'
|
||||
import 'moment/locale/ru'
|
||||
import 'moment/locale/vi'
|
||||
import 'moment/locale/zh-cn'
|
||||
import 'moment/locale/zh-tw'
|
||||
|
||||
export default {
|
||||
props: ['expiry'],
|
||||
emits: ['submit'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
input: new Date(),
|
||||
}
|
||||
},
|
||||
components: { DatePicker },
|
||||
computed: {
|
||||
locale() {
|
||||
return locale
|
||||
},
|
||||
dateFormatted() {
|
||||
if (this.expDate == 0) return i18n.global.t('unlimited')
|
||||
const date = new Date(this.expDate*1000)
|
||||
return date.toLocaleString(locale)
|
||||
},
|
||||
expDate() {
|
||||
return parseInt(this.expiry?? 0)
|
||||
},
|
||||
Input: {
|
||||
get() { return this.expDate == 0 ? new Date() : new Date(this.expDate*1000) },
|
||||
set(v:string) {
|
||||
this.input = new Date(v)
|
||||
this.submit()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateInput(v:Date) {
|
||||
this.input = v
|
||||
},
|
||||
setNow() {
|
||||
this.input = new Date()
|
||||
},
|
||||
submit() {
|
||||
this.$emit('submit',Math.floor(this.input.getTime()/1000))
|
||||
},
|
||||
reset(vm:any) {
|
||||
this.$emit('submit',0)
|
||||
this.input = new Date()
|
||||
vm.visible = false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
menu(v) {
|
||||
if (v) {
|
||||
this.input = this.expiry == 0 ? new Date() : new Date(this.expDate*1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.vpd-addon-list,
|
||||
.vpd-addon-list-item {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
border-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
.vpd-content {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
.vpd-addon-list-item.vpd-selected,
|
||||
.vpd-addon-list-item:hover {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
.vpd-close-addon {
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
.vpd-controls {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.vpd-month-label {
|
||||
width: auto;
|
||||
}
|
||||
.vpd-actions button:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
.vpd-wrapper[data-type=datetime].vpd-compact-time .vpd-time {
|
||||
border-top: 0;
|
||||
}
|
||||
.vpd-time .vpd-time-h .vpd-counter-item,
|
||||
.vpd-time .vpd-time-m .vpd-counter-item {
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.dial')" style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('dial.detourText')"
|
||||
:items="outTags"
|
||||
v-model="dial.detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionBind">
|
||||
<v-text-field
|
||||
:label="$t('dial.bindIf')"
|
||||
hide-details
|
||||
v-model="dial.bind_interface"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIPV4">
|
||||
<v-text-field
|
||||
:label="$t('dial.bindIp4')"
|
||||
hide-details
|
||||
v-model="dial.inet4_bind_address"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIPV6">
|
||||
<v-text-field
|
||||
:label="$t('dial.bindIp6')"
|
||||
hide-details
|
||||
v-model="dial.inet6_bind_address"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionBindNoPort">
|
||||
<v-switch v-model="dial.bind_address_no_port" color="primary" :label="$t('dial.bindNoPort')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionRM">
|
||||
<v-text-field
|
||||
label="Linux Routing Mark"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="routingMark"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionRA">
|
||||
<v-switch v-model="dial.reuse_addr" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionTCP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dial.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dial.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionTcpKeepAlive">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dial.disable_tcp_keep_alive" color="primary" :label="$t('dial.disableTcpKeepAlive')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dial.tcp_keep_alive" :label="$t('dial.tcpKeepAlive')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dial.tcp_keep_alive_interval" :label="$t('dial.tcpKeepAliveInterval')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionUDP">
|
||||
<v-switch v-model="dial.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionCT">
|
||||
<v-text-field
|
||||
:label="$t('dial.connTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
min="1"
|
||||
:suffix="$t('date.s')"
|
||||
v-model.number="connectTimeout"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionDR">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('dial.domainResolver')"
|
||||
:items="dnsTags"
|
||||
v-model="dial.domain_resolver">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('dial.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionBind" color="primary" :label="$t('dial.bindIf')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionIPV4" color="primary" :label="$t('dial.bindIp4')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionIPV6" color="primary" :label="$t('dial.bindIp6')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionBindNoPort" color="primary" :label="$t('dial.bindNoPort')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionRM" color="primary" label="Routing Mark" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionRA" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCT" color="primary" :label="$t('dial.connTimeout')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTcpKeepAlive" color="primary" :label="$t('dial.tcpKeepAlive')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mode != 'client'">
|
||||
<v-switch v-model="optionDR" color="primary" :label="$t('dial.domainResolver')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Data from '@/store/modules/data'
|
||||
|
||||
export default {
|
||||
props: ['dial', 'mode'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
outTags() { return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)] },
|
||||
connectTimeout: {
|
||||
get() { return this.$props.dial.connect_timeout ? parseInt(this.$props.dial.connect_timeout.replace('s','')) : 5 },
|
||||
set(newValue:number) { this.$props.dial.connect_timeout = newValue > 0 ? newValue + 's' : '5s' }
|
||||
},
|
||||
routingMark: {
|
||||
get() { return this.$props.dial.routing_mark?? 0 },
|
||||
set(newValue:number) { this.$props.dial.routing_mark = newValue > 0 ? newValue : 0 }
|
||||
},
|
||||
optionDetour: {
|
||||
get(): boolean { return this.$props.dial.detour != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.detour = this.outTags[0]?? '' : delete this.$props.dial.detour }
|
||||
},
|
||||
optionBind: {
|
||||
get(): boolean { return this.$props.dial.bind_interface != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.bind_interface = '' : delete this.$props.dial.bind_interface }
|
||||
},
|
||||
optionIPV4: {
|
||||
get(): boolean { return this.$props.dial.inet4_bind_address != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.inet4_bind_address = '' : delete this.$props.dial.inet4_bind_address }
|
||||
},
|
||||
optionIPV6: {
|
||||
get(): boolean { return this.$props.dial.inet6_bind_address != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.inet6_bind_address = '' : delete this.$props.dial.inet6_bind_address }
|
||||
},
|
||||
optionBindNoPort: {
|
||||
get(): boolean { return this.$props.dial.bind_address_no_port != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.bind_address_no_port = true : delete this.$props.dial.bind_address_no_port }
|
||||
},
|
||||
optionTcpKeepAlive: {
|
||||
get(): boolean {
|
||||
return this.$props.dial.disable_tcp_keep_alive != undefined ||
|
||||
this.$props.dial.tcp_keep_alive != undefined ||
|
||||
this.$props.dial.tcp_keep_alive_interval != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.dial.tcp_keep_alive = '5m'
|
||||
this.$props.dial.tcp_keep_alive_interval = '75s'
|
||||
} else {
|
||||
delete this.$props.dial.disable_tcp_keep_alive
|
||||
delete this.$props.dial.tcp_keep_alive
|
||||
delete this.$props.dial.tcp_keep_alive_interval
|
||||
}
|
||||
}
|
||||
},
|
||||
optionRM: {
|
||||
get(): boolean { return this.$props.dial.routing_mark != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.routing_mark = 0 : delete this.$props.dial.routing_mark }
|
||||
},
|
||||
optionRA: {
|
||||
get(): boolean { return this.$props.dial.reuse_addr != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.reuse_addr = true : delete this.$props.dial.reuse_addr }
|
||||
},
|
||||
optionTCP: {
|
||||
get(): boolean {
|
||||
return this.$props.dial.tcp_fast_open != undefined &&
|
||||
this.$props.dial.tcp_multi_path != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.dial.tcp_fast_open = false
|
||||
this.$props.dial.tcp_multi_path = false
|
||||
} else {
|
||||
delete this.$props.dial.tcp_fast_open
|
||||
delete this.$props.dial.tcp_multi_path
|
||||
}
|
||||
}
|
||||
},
|
||||
optionUDP: {
|
||||
get(): boolean { return this.$props.dial.udp_fragment != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.udp_fragment = true : delete this.$props.dial.udp_fragment }
|
||||
},
|
||||
optionCT: {
|
||||
get(): boolean { return this.$props.dial.connect_timeout != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.connect_timeout = '5s' : delete this.$props.dial.connect_timeout }
|
||||
},
|
||||
optionDR: {
|
||||
get(): boolean { return this.$props.dial.domain_resolver != undefined },
|
||||
set(v:boolean) { this.$props.dial.domain_resolver = v ? this.dnsTags[0]?? '' : undefined }
|
||||
},
|
||||
dnsTags() {return Data().config.dns?.servers?.map((d:any) => d.tag) ?? []}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<v-card style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" v-if="optionInbound">
|
||||
<v-combobox
|
||||
v-model="rule.inbound"
|
||||
:items="inTags"
|
||||
:label="$t('pages.inbounds')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" v-if="optionClient">
|
||||
<v-combobox
|
||||
v-model="rule.auth_user"
|
||||
:items="clients"
|
||||
:label="$t('pages.clients')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIPver">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('rule.ipVer')"
|
||||
:items="[4,6]"
|
||||
v-model.number="rule.ip_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="optionProtocol">
|
||||
<v-combobox
|
||||
v-model="rule.protocol"
|
||||
:items="['http','tls', 'quic', 'stun', 'dns']"
|
||||
:label="$t('protocol')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionDomain">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="domainKeys"
|
||||
@update:model-value="updateDomainOption($event)"
|
||||
v-model="domainOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="domain"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain_suffix != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.domainSufix') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="domain_suffix"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain_keyword != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.domainKw') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="domain_keyword"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain_regex != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.domainRgx') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="domain_regex"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionPort">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="portKeys"
|
||||
@update:model-value="updatePortOption($event)"
|
||||
v-model="portOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.port != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.port') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="port"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.port_range != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.portRange') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="port_range"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionSrcIP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="srcIPKeys"
|
||||
@update:model-value="updateSrcIPOption($event)"
|
||||
v-model="srcIPOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionSrcPort">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="srcPortKeys"
|
||||
@update:model-value="updateSrcPortOption($event)"
|
||||
v-model="srcPortOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.source_port != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.srcPort') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="source_port"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.source_port_range != undefined">
|
||||
<v-text-field
|
||||
:label="$t('rule.srcPortRange') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="source_port_range"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionRuleSet">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-combobox
|
||||
v-model="rule.rule_set"
|
||||
:items="ruleSets"
|
||||
:label="$t('rule.ruleset')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('rule.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionInbound" color="primary" :label="$t('pages.inbounds')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionClient" color="primary" :label="$t('pages.clients')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionIPver" color="primary" :label="$t('rule.ipVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionProtocol" color="primary" :label="$t('protocol')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDomain" color="primary" :label="$t('rule.domainRules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionPort" color="primary" :label="$t('in.port')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSrcIP" color="primary" :label="$t('rule.srcIpRules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSrcPort" color="primary" :label="$t('rule.srcPortRules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRuleSet" color="primary" :label="$t('rule.ruleset')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['rule', 'clients', 'inTags', 'rsTags', 'deleteable', 'ruleSets'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
domainKeys: ['domain', 'domain_suffix', 'domain_keyword', 'domain_regex'],
|
||||
portKeys: ['port', 'port_range'],
|
||||
srcIPKeys: ['source_ip_cidr', 'source_ip_is_private'],
|
||||
srcPortKeys: ['source_port', 'source_port_range'],
|
||||
domainOption: 'domain',
|
||||
portOption: 'port',
|
||||
srcIPOption: 'source_ip_cidr',
|
||||
srcPortOption: 'source_port',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateDomainOption(option:string) {
|
||||
this.domainKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = []
|
||||
},
|
||||
updatePortOption(option:string) {
|
||||
this.portKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = []
|
||||
},
|
||||
updateSrcIPOption(option:string) {
|
||||
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = option == 'source_ip_is_private' ? false : []
|
||||
},
|
||||
updateSrcPortOption(option:string) {
|
||||
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = []
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
optionInbound: {
|
||||
get() { return this.$props.rule.inbound != undefined },
|
||||
set(v:boolean) { this.$props.rule.inbound = v ? [] : undefined }
|
||||
},
|
||||
optionClient: {
|
||||
get() { return this.$props.rule.auth_user != undefined },
|
||||
set(v:boolean) { this.$props.rule.auth_user = v ? [] : undefined }
|
||||
},
|
||||
optionIPver: {
|
||||
get() { return this.$props.rule.ip_version != undefined },
|
||||
set(v:boolean) { this.$props.rule.ip_version = v ? 4 : undefined }
|
||||
},
|
||||
optionProtocol: {
|
||||
get() { return this.$props.rule.protocol != undefined },
|
||||
set(v:boolean) { this.$props.rule.protocol = v ? ['http'] : undefined }
|
||||
},
|
||||
optionDomain: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.domainKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.domain = []
|
||||
} else {
|
||||
this.domainKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.domainOption = 'domain'
|
||||
}
|
||||
},
|
||||
optionPort: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.portKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.port = []
|
||||
} else {
|
||||
this.portKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.portOption = 'port'
|
||||
}
|
||||
},
|
||||
optionSrcIP: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.srcIPKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.source_ip_cidr = []
|
||||
} else {
|
||||
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.srcIPOption = 'source_ip_cidr'
|
||||
}
|
||||
},
|
||||
optionSrcPort: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.srcPortKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.source_port = []
|
||||
} else {
|
||||
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.srcPortOption = 'source_port'
|
||||
}
|
||||
},
|
||||
optionRuleSet: {
|
||||
get() { return this.$props.rule.rule_set != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.rule_set = []
|
||||
} else {
|
||||
delete this.$props.rule.rule_set
|
||||
}
|
||||
}
|
||||
},
|
||||
domain: {
|
||||
get() { return this.$props.rule.domain?.join(',') },
|
||||
set(v:string) { this.$props.rule.domain = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
domain_suffix: {
|
||||
get() { return this.$props.rule.domain_suffix?.join(',') },
|
||||
set(v:string) { this.$props.rule.domain_suffix = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
domain_keyword: {
|
||||
get() { return this.$props.rule.domain_keyword?.join(',') },
|
||||
set(v:string) { this.$props.rule.domain_keyword = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
domain_regex: {
|
||||
get() { return this.$props.rule.domain_regex?.join(',') },
|
||||
set(v:string) { this.$props.rule.domain_regex = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
ip_cidr: {
|
||||
get() { return this.$props.rule.ip_cidr?.join(',') },
|
||||
set(v:string) { this.$props.rule.ip_cidr = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
port: {
|
||||
get() { return this.$props.rule.port?.join(',') },
|
||||
set(v:string) {
|
||||
if(!v.endsWith(',')) {
|
||||
this.$props.rule.port = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
|
||||
}
|
||||
}
|
||||
},
|
||||
port_range: {
|
||||
get() { return this.$props.rule.port_range?.join(',') },
|
||||
set(v:string) { this.$props.rule.port_range = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
source_ip_cidr: {
|
||||
get() { return this.$props.rule.source_ip_cidr?.join(',') },
|
||||
set(v:string) { this.$props.rule.source_ip_cidr = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
source_port: {
|
||||
get() { return this.$props.rule.source_port?.join(',') },
|
||||
set(v:string) {
|
||||
if(!v.endsWith(',')) {
|
||||
this.$props.rule.source_port = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
|
||||
}
|
||||
}
|
||||
},
|
||||
source_port_range: {
|
||||
get() { return this.$props.rule.source_port_range?.join(',') },
|
||||
set(v:string) { this.$props.rule.source_port_range = v.length>0 ? v.split(',') : [] }
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const ruleKeys = Object.keys(this.$props.rule)
|
||||
if (this.optionDomain) {
|
||||
const enabledOption = this.domainKeys.filter(k => ruleKeys.includes(k))
|
||||
this.domainOption = enabledOption.length>0 ? enabledOption[0] : 'domain'
|
||||
}
|
||||
if (this.optionPort) {
|
||||
const enabledOption = this.portKeys.filter(k => ruleKeys.includes(k))
|
||||
this.portOption = enabledOption.length>0 ? enabledOption[0] : 'port'
|
||||
}
|
||||
if (this.optionSrcIP) {
|
||||
const enabledOption = this.srcIPKeys.filter(k => ruleKeys.includes(k))
|
||||
this.srcIPOption = enabledOption.length>0 ? enabledOption[0] : 'source_ip_cidr'
|
||||
}
|
||||
if (this.optionSrcPort) {
|
||||
const enabledOption = this.srcPortKeys.filter(k => ruleKeys.includes(k))
|
||||
this.srcPortOption = enabledOption.length>0 ? enabledOption[0] : 'source_port'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<div class="code-editor">
|
||||
<div class="line-numbers">
|
||||
<span v-for="n in lineCount" :key="n">{{ n }}</span>
|
||||
</div>
|
||||
<v-textarea
|
||||
ref="textareaRef"
|
||||
v-model="content"
|
||||
@scroll="syncScroll"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
bg-color="background"
|
||||
:style="{ 'font-family': 'monospace' }"
|
||||
no-resize
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'data', 'title'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
content: this.$props.data,
|
||||
theme: useTheme()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lineCount() {
|
||||
return this.content?.split('\n').length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
syncScroll() {
|
||||
const textarea = document.querySelector('textarea')
|
||||
const lineNumbers = textarea?.parentElement?.parentElement?.querySelector('.line-numbers')
|
||||
if (lineNumbers && textarea) {
|
||||
lineNumbers.scrollTop = textarea.scrollTop
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.$emit('save', this.content)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.content = this.$props.data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.code-editor {
|
||||
direction: ltr !important;
|
||||
display: flex;
|
||||
border: 1px solid v-bind('theme.current.colors["outline"]');
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
font-size: 14px; /* Consistent font size */
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
width: 40px;
|
||||
background: v-bind('theme.current.colors["surface"]');
|
||||
text-align: right;
|
||||
padding: 12px 8px 12px 4px; /* Match textarea padding */
|
||||
line-height: 1.5; /* Match textarea line height */
|
||||
overflow-y: hidden; /* Prevent independent scrolling */
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.line-numbers span {
|
||||
display: block;
|
||||
line-height: 1.5; /* Match textarea line height */
|
||||
height: 1.5em; /* Ensure consistent height per line */
|
||||
font-family: monospace; /* Match textarea font */
|
||||
}
|
||||
|
||||
/* Override Vuetify textarea styles for alignment */
|
||||
:deep(.v-textarea .v-field__input) {
|
||||
padding: 12px 8px !important; /* Match line-numbers padding */
|
||||
line-height: 1.5 !important; /* Match line-numbers line height */
|
||||
font-family: monospace !important;
|
||||
white-space: pre;
|
||||
mask-image: inherit;
|
||||
font-size: 14px !important; /* Match font size */
|
||||
}
|
||||
|
||||
/* Ensure textarea and line numbers align */
|
||||
:deep(.v-textarea textarea) {
|
||||
margin-top: 0 !important; /* Remove any default margin */
|
||||
padding-top: 0 !important; /* Remove any default padding */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="620">
|
||||
<v-card>
|
||||
<v-card-title>{{ label }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>{{ $t('rule.etaHint') }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea
|
||||
v-model="localText"
|
||||
:label="label"
|
||||
variant="outlined"
|
||||
rows="16"
|
||||
:counter="$t('count')"
|
||||
persistent-counter
|
||||
:counter-value="(v: string) => v.split('\n').filter((l: string) => l.trim().length > 0).length"
|
||||
spellcheck="false"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn @click="resetChanges" color="error" variant="plain">{{ $t('reset') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn @click="closeModal" color="primary" variant="outlined">{{ $t('actions.close') }}</v-btn>
|
||||
<v-btn @click="saveChanges" color="primary" variant="tonal">{{ $t('actions.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['visible', 'label', 'content'],
|
||||
emits: ['update', 'close'],
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
localText: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.localText = this.content
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
saveChanges() {
|
||||
const unique = [
|
||||
...new Set(
|
||||
this.localText
|
||||
.split('\n')
|
||||
.map((l: string) => l.trim())
|
||||
.filter((l: string) => l.length > 0)
|
||||
),
|
||||
]
|
||||
this.$emit('update', unique)
|
||||
this.dialog = false
|
||||
},
|
||||
resetChanges() {
|
||||
this.localText = this.content
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-card-subtitle>
|
||||
{{ $t('objects.headers') }}
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="add_header">
|
||||
<v-icon icon="mdi-plus" />
|
||||
</v-chip>
|
||||
</v-card-subtitle>
|
||||
<v-row v-for="(header, index) in hdrs">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('objects.key')"
|
||||
hide-details
|
||||
@input="update_key(index,$event.target.value)"
|
||||
v-model="header.name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('objects.value')"
|
||||
hide-details
|
||||
@input="update_value(index,$event.target.value)"
|
||||
v-model="header.value">
|
||||
<template v-slot:append>
|
||||
<v-icon @click="del_header(index)" color="error" icon="mdi-delete" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
type Header = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
add_header() {
|
||||
this.hdrs = [...this.hdrs, {name: "Host", value: ""}]
|
||||
},
|
||||
del_header(i:number) {
|
||||
let h = this.hdrs
|
||||
h.splice(i,1)
|
||||
this.hdrs = h
|
||||
},
|
||||
update_key(i:number,k:string) {
|
||||
let h = this.hdrs
|
||||
h[i].name = k
|
||||
this.hdrs = h
|
||||
},
|
||||
update_value(i:number,v:string) {
|
||||
let h = this.hdrs
|
||||
h[i].value = v
|
||||
this.hdrs = h
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hdrs: {
|
||||
get() :Header[] {
|
||||
let headers: Header[] = []
|
||||
const h = this.$props.data.headers
|
||||
if (h) {
|
||||
Object.keys(h).forEach(key => {
|
||||
if (Array.isArray(h[key])){
|
||||
h[key].forEach((v:string) => headers.push({ name: key, value: v }))
|
||||
} else {
|
||||
headers.push({ name: key, value: h[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
return headers
|
||||
},
|
||||
set(v:Header[]) {
|
||||
if (v.length>0) {
|
||||
let headers:any = {}
|
||||
v.forEach((h:Header) => {
|
||||
if (headers[h.name]) {
|
||||
if (Array.isArray(headers[h.name])) {
|
||||
headers[h.name].push(h.value)
|
||||
} else {
|
||||
headers[h.name] = [headers[h.name], h.value]
|
||||
}
|
||||
} else {
|
||||
headers[h.name] = h.value
|
||||
}
|
||||
})
|
||||
this.$props.data.headers = headers
|
||||
} else {
|
||||
this.$props.data.headers = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.listen')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('in.addr')"
|
||||
hide-details
|
||||
required
|
||||
v-model="data.listen">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('in.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
required
|
||||
v-model.number="data.listen_port"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
|
||||
<v-select
|
||||
:label="$t('listen.detourText')"
|
||||
hide-details
|
||||
:items="inTags"
|
||||
v-model="data.detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionTCP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionUDP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="UDP NAT expiration"
|
||||
hide-details
|
||||
type="number"
|
||||
min="1"
|
||||
:suffix="$t('date.m')"
|
||||
v-model.number="udpTimeout"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionTcpKeepAlive">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.disable_tcp_keep_alive" color="primary" :label="$t('listen.disableTcpKeepAlive')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.tcp_keep_alive" :label="$t('listen.tcpKeepAlive')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.tcp_keep_alive_interval" :label="$t('listen.tcpKeepAliveInterval')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('listen.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTcpKeepAlive" color="primary" :label="$t('listen.tcpKeepAlive')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data', 'inTags'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
udpTimeout: {
|
||||
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
|
||||
set(newValue:number) { this.$props.data.udp_timeout = newValue > 0 ? newValue + 'm' : '5m' }
|
||||
},
|
||||
optionTCP: {
|
||||
get(): boolean {
|
||||
return this.$props.data.tcp_fast_open != undefined &&
|
||||
this.$props.data.tcp_multi_path != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
this.$props.data.tcp_fast_open = v ? false : undefined
|
||||
this.$props.data.tcp_multi_path = v ? false : undefined
|
||||
}
|
||||
},
|
||||
optionUDP: {
|
||||
get(): boolean {
|
||||
return this.$props.data.udp_fragment != undefined &&
|
||||
this.$props.data.udp_timeout != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
this.$props.data.udp_fragment = v ? false : undefined
|
||||
this.$props.data.udp_timeout = v ? '5m' : undefined
|
||||
}
|
||||
},
|
||||
optionDetour: {
|
||||
get(): boolean { return this.$props.data.detour != undefined },
|
||||
set(v:boolean) { this.$props.data.detour = v ? this.inTags[0]?? '' : undefined }
|
||||
},
|
||||
optionTcpKeepAlive: {
|
||||
get(): boolean {
|
||||
return this.$props.data.disable_tcp_keep_alive != undefined ||
|
||||
this.$props.data.tcp_keep_alive != undefined ||
|
||||
this.$props.data.tcp_keep_alive_interval != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.data.tcp_keep_alive = '5m'
|
||||
this.$props.data.tcp_keep_alive_interval = '75s'
|
||||
} else {
|
||||
delete this.$props.data.disable_tcp_keep_alive
|
||||
delete this.$props.data.tcp_keep_alive
|
||||
delete this.$props.data.tcp_keep_alive_interval
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<LogVue v-model="logModal.visible" :control="logModal" :visible="logModal.visible" />
|
||||
<Backup v-model="backupModal.visible" :control="backupModal" :visible="backupModal.visible" />
|
||||
<UsageStats v-model:visible="usageStatsModal.visible" />
|
||||
<v-container class="fill-height" :loading="loading">
|
||||
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
<v-col cols="auto">
|
||||
<v-img src="@/assets/logo.svg" :width="reloadItems.length>0 ? 100 : 200"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
<v-col cols="auto">
|
||||
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal" elevation="3">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
|
||||
</template>
|
||||
<v-card rounded="xl">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
{{ $t('main.tiles') }}
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close" @click="menu = false"></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-row v-for="items in menuItems" density="compact">
|
||||
<v-col cols="12">
|
||||
<v-card :subtitle="items.title" variant="flat">
|
||||
<v-card-text>
|
||||
<v-row density="compact">
|
||||
<v-col cols="12" md="6" lg="3" v-for="item in items.value">
|
||||
<v-switch
|
||||
density="compact"
|
||||
v-model="reloadItems"
|
||||
:value="item.value"
|
||||
color="primary"
|
||||
:label="item.title"
|
||||
hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-btn variant="tonal" hide-details
|
||||
style="margin-inline-start: 10px;" elevation="3"
|
||||
@click="backupModal.visible = true">{{ $t('main.backup.title') }}<v-icon icon="mdi-backup-restore" />
|
||||
</v-btn>
|
||||
<v-btn variant="tonal" hide-details
|
||||
style="margin-inline-start: 10px;" elevation="3"
|
||||
@click="logModal.visible = true">{{ $t('basic.log.title') }} <v-icon icon="mdi-list-box-outline" />
|
||||
</v-btn>
|
||||
<v-btn variant="tonal" hide-details
|
||||
style="margin-inline-start: 10px;" elevation="3"
|
||||
@click="usageStatsModal.visible = true">{{ $t('main.stats.title') }} <v-icon icon="mdi-chart-box-outline" />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
|
||||
<v-card class="rounded-lg" variant="outlined" height="210px" elevation="5">
|
||||
<v-card-title>
|
||||
{{ menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title }}
|
||||
<template v-if="i == 'i-sys'">
|
||||
<v-icon icon="mdi-update" color="primary"
|
||||
@click="reloadSys()" size="small" v-tooltip:top="$t('actions.update')"
|
||||
style="margin-inline-start: 10px;">
|
||||
</v-icon>
|
||||
</template>
|
||||
<template v-if="i == 'h-net'">
|
||||
<v-icon icon="mdi-information" color="primary" size="small"
|
||||
v-tooltip:top="'↓' +
|
||||
HumanReadable.sizeFormat(tilesData.net?.recv) + ' - ' +
|
||||
HumanReadable.sizeFormat(tilesData.net?.sent) + '↑'"
|
||||
style="margin-inline-start: 10px;">
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-card-title>
|
||||
<v-card-text style="padding: 0 16px;" align="center" justify="center">
|
||||
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
|
||||
<History :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'h'" />
|
||||
<template v-if="i == 'i-sys'">
|
||||
<v-row>
|
||||
<v-col cols="3">{{ $t('main.info.host') }}</v-col>
|
||||
<v-col cols="9" style="text-wrap: nowrap; overflow: hidden">{{ tilesData.sys?.hostName }}</v-col>
|
||||
<v-col cols="3">{{ $t('main.info.cpu') }}</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" variant="flat">
|
||||
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||
{{ tilesData.sys?.cpuType }}
|
||||
</v-tooltip>
|
||||
{{ tilesData.sys?.cpuCount }} {{ $t('main.info.core') }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">IP</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv4?.length>0">
|
||||
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||
<span v-html="tilesData.sys?.ipv4?.join('<br />')"></span>
|
||||
</v-tooltip>
|
||||
IPv4
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv6?.length>0">
|
||||
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||
<span v-html="tilesData.sys?.ipv6?.join('<br />')"></span>
|
||||
</v-tooltip>
|
||||
IPv6
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">S-UI</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" color="blue">
|
||||
v{{ tilesData.sys?.appVersion }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
|
||||
<v-col cols="9" v-tooltip:top="$t('main.info.startupTime')
|
||||
+ ': ' + new Date((tilesData.sys?.bootTime || 0) * 1000).toLocaleString(locale)">
|
||||
{{ HumanReadable.formatSecond((Date.now()/1000) - tilesData.sys?.bootTime) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="i == 'i-sbd'">
|
||||
<v-row>
|
||||
<v-col cols="4">{{ $t('main.info.running') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip>
|
||||
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
|
||||
<v-chip density="compact" color="transparent" v-if="tilesData.sbd?.running && !loading" style="cursor: pointer;" @click="restartSingbox()">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.restartSb') }}
|
||||
</v-tooltip>
|
||||
<v-icon icon="mdi-restart" color="warning" />
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.memory') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.Alloc">
|
||||
{{ HumanReadable.sizeFormat(tilesData.sbd?.stats?.Alloc) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.threads') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.NumGoroutine">
|
||||
{{ tilesData.sbd?.stats?.NumGoroutine }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.uptime') }}</v-col>
|
||||
<v-col cols="8">{{ HumanReadable.formatSecond(tilesData.sbd?.stats?.Uptime) }}</v-col>
|
||||
<v-col cols="4">{{ $t('online') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<template v-if="tilesData.sbd?.running">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="Data().onlines.user">
|
||||
<v-tooltip activator="parent" location="top" overflow="auto">
|
||||
<span v-text="$t('pages.clients')" style="font-weight: bold;"></span><br/>
|
||||
<span v-for="user in Data().onlines.user">{{ user }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ Data().onlines.user?.length }}
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="success" variant="flat" v-if="Data().onlines.inbound">
|
||||
<v-tooltip activator="parent" location="top" :text="$t('pages.inbounds')">
|
||||
<span v-text="$t('pages.inbounds')" style="font-weight: bold;"></span><br/>
|
||||
<span v-for="i in Data().onlines.inbound">{{ i }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ Data().onlines.inbound?.length }}
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="info" variant="flat" v-if="Data().onlines.outbound">
|
||||
<v-tooltip activator="parent" location="top" :text="$t('pages.outbounds')">
|
||||
<span v-text="$t('pages.outbounds')" style="font-weight: bold;"></span><br/>
|
||||
<span v-for="o in Data().onlines.outbound">{{ o }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ Data().onlines.outbound?.length }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import Data from '@/store/modules/data'
|
||||
import Gauge from '@/components/tiles/Gauge.vue'
|
||||
import History from '@/components/tiles/History.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { i18n, locale } from '@/locales'
|
||||
import LogVue from '@/layouts/modals/Logs.vue'
|
||||
import Backup from '@/layouts/modals/Backup.vue'
|
||||
import UsageStats from '@/layouts/modals/UsageStats.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const menu = ref(false)
|
||||
const menuItems = [
|
||||
{ title: i18n.global.t('main.gauges'), value: [
|
||||
{ title: i18n.global.t('main.gauge.cpu'), value: "g-cpu" },
|
||||
{ title: i18n.global.t('main.gauge.mem'), value: "g-mem" },
|
||||
{ title: i18n.global.t('main.gauge.dsk'), value: "g-dsk" },
|
||||
{ title: i18n.global.t('main.gauge.swp'), value: "g-swp" },
|
||||
]
|
||||
},
|
||||
{ title: i18n.global.t('main.charts'), value: [
|
||||
{ title: i18n.global.t('main.chart.cpu'), value: "h-cpu" },
|
||||
{ title: i18n.global.t('main.chart.mem'), value: "h-mem" },
|
||||
{ title: i18n.global.t('main.chart.net'), value: "h-net" },
|
||||
{ title: i18n.global.t('main.chart.pnet'), value: "hp-net" },
|
||||
{ title: i18n.global.t('main.chart.dio'), value: "h-dio" },
|
||||
]
|
||||
},
|
||||
{ title: i18n.global.t('main.infos'), value: [
|
||||
{ title: i18n.global.t('main.info.sys'), value: "i-sys" },
|
||||
{ title: i18n.global.t('main.info.sbd'), value: "i-sbd" },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
const tilesData = ref(<any>{})
|
||||
|
||||
const reloadItems = computed({
|
||||
get() { return Data().reloadItems },
|
||||
set(v:string[]) {
|
||||
if (Data().reloadItems.length == 0 && v.length>0) startTimer()
|
||||
if (Data().reloadItems.length > 0 && v.length == 0) stopTimer()
|
||||
Data().reloadItems = v
|
||||
v.length>0 ? localStorage.setItem("reloadItems",v.join(',')) : localStorage.removeItem("reloadItems")
|
||||
}
|
||||
})
|
||||
|
||||
const reloadData = async () => {
|
||||
const request = [...new Set(reloadItems.value.map(r => r.split('-')[1]))]
|
||||
if (tilesData.value?.sys?.appVersion) request.filter(r => r != 'sys')
|
||||
const data = await HttpUtils.get('api/status',{ r: request.join(',')})
|
||||
if (data.success) {
|
||||
tilesData.value = data.obj
|
||||
}
|
||||
}
|
||||
|
||||
const reloadSys = async () => {
|
||||
const data = await HttpUtils.get('api/status',{ r: 'sys'})
|
||||
if (data.success) {
|
||||
tilesData.value.sys = data.obj.sys
|
||||
}
|
||||
}
|
||||
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const startTimer = () => {
|
||||
intervalId = setInterval(() => {
|
||||
reloadData()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const stopTimer = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
if (Data().reloadItems.length != 0) {
|
||||
await reloadData()
|
||||
startTimer()
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTimer()
|
||||
})
|
||||
|
||||
const logModal = ref({ visible: false })
|
||||
|
||||
const backupModal = ref({ visible: false })
|
||||
|
||||
const usageStatsModal = ref({ visible: false })
|
||||
|
||||
const restartSingbox = async () => {
|
||||
loading.value = true
|
||||
await HttpUtils.post('api/restartSb',{})
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.multiplex')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('mux.enable')" v-model="muxEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
<template v-if="muxEnable">
|
||||
<template v-if="direction=='out'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="[ 'smux', 'yamux', 'h2mux']"
|
||||
:label="$t('protocol')"
|
||||
clearable
|
||||
@click:clear="delete mux?.protocol"
|
||||
v-model="mux.protocol">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('mux.maxConn')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
v-model.number="max_connections">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('mux.minStr')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
v-model.number="min_streams">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('mux.maxStr')"
|
||||
hide-details
|
||||
type="number"
|
||||
:min="min_streams"
|
||||
v-model.number="max_streams">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('mux.padding')" v-model="padding" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('mux.enableBrutal')" v-model="burtalEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-row v-if="mux?.brutal?.enabled">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('stats.upload')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('stats.Mbps')"
|
||||
v-model.number="up_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('stats.download')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('stats.Mbps')"
|
||||
min="0"
|
||||
v-model.number="down_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { oMultiplex } from '@/types/multiplex'
|
||||
export default {
|
||||
props: ['data', 'direction'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
mux(): oMultiplex {
|
||||
return <oMultiplex> this.$props.data.multiplex ?? null
|
||||
},
|
||||
muxEnable: {
|
||||
get(): boolean { return this.mux ? this.mux.enabled : false },
|
||||
set(newValue:boolean) { this.$props.data.multiplex = newValue ? { enabled: newValue } : undefined }
|
||||
},
|
||||
max_connections: {
|
||||
get(): number { return this.mux?.max_connections ? this.mux.max_connections : 0 },
|
||||
set(newValue:number) { this.mux.max_connections = newValue > 0 ? newValue : undefined }
|
||||
},
|
||||
min_streams: {
|
||||
get(): number { return this.mux?.min_streams ? this.mux.min_streams : 0 },
|
||||
set(newValue:number) { this.mux.min_streams = newValue > 0 ? newValue : undefined }
|
||||
},
|
||||
max_streams: {
|
||||
get(): number { return this.mux?.max_streams ? this.mux.max_streams : 0 },
|
||||
set(newValue:number) { this.mux.max_streams = newValue > 0 ? newValue : undefined }
|
||||
},
|
||||
padding: {
|
||||
get(): boolean { return this.mux?.padding ? this.mux.padding : false },
|
||||
set(newValue:boolean) { this.mux.padding = newValue ? true : undefined }
|
||||
},
|
||||
burtalEnable: {
|
||||
get(): boolean { return this.mux?.brutal ? this.mux.brutal.enabled : false },
|
||||
set(newValue:boolean) { this.mux.brutal = newValue ? { enabled: newValue, up_mbps: 100, down_mbps: 100 } : undefined }
|
||||
},
|
||||
down_mbps: {
|
||||
get() { return this.mux?.brutal && this.mux.brutal.down_mbps ? this.mux.brutal.down_mbps : 0 },
|
||||
set(newValue:any) {
|
||||
if (this.mux.brutal){
|
||||
this.mux.brutal.down_mbps = newValue.length != 0 ? newValue : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
up_mbps: {
|
||||
get() { return this.mux?.brutal && this.mux.brutal.up_mbps ? this.mux.brutal.up_mbps : 0 },
|
||||
set(newValue:any) {
|
||||
if (this.mux.brutal){
|
||||
this.mux.brutal.up_mbps = newValue.length != 0 ? newValue : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('network')"
|
||||
:items="networks"
|
||||
v-model="Network">
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
networks: [
|
||||
{ title: "TCP/UDP", value: '' },
|
||||
{ title: "TCP", value: 'tcp' },
|
||||
{ title: "UDP", value: 'udp' },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Network: {
|
||||
get():string { return this.$props.data.network?? '' },
|
||||
set(v:string) { this.$props.data.network = v != '' ? v : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('pages.basics')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.SOCKS">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="['4','4a','5']"
|
||||
:label="$t('version')"
|
||||
v-model="inData.out_json.version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
|
||||
<Network :data="inData.out_json" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="needUot">
|
||||
<UoT :data="inData.out_json" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="inData.out_json.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vless.udpEnc')"
|
||||
:items="['none','packetaddr','xudp']"
|
||||
v-model="packet_encoding">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<template v-if="type == inTypes.VMess">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vmess.security')"
|
||||
:items="vmessSecurities"
|
||||
v-model="inData.out_json.security">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inData.out_json.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inData.out_json.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</template>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
|
||||
<v-text-field
|
||||
label="Recv window"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="inData.out_json.recv_window">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<template v-if="type == inTypes.TUIC">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="UDP Relay Mode"
|
||||
:items="['native', 'quic']"
|
||||
clearable
|
||||
@click:clear="delete inData.out_json.udp_relay_mode"
|
||||
v-model="inData.out_json.udp_relay_mode">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="UDP Over Stream" v-model="inData.out_json.udp_over_stream" hide-details></v-switch>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-row v-if="[inTypes.Hysteria, inTypes.Hysteria2].includes(type)">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
:label="$t('rule.portRange') + ' ' + $t('commaSeparated')"
|
||||
v-model="server_ports">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('ruleset.interval')"
|
||||
type="number"
|
||||
min="0"
|
||||
:suffix="$t('date.s')"
|
||||
v-model.number="hop_interval">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="inData.out_json" v-if="type == inTypes.HTTP" />
|
||||
<AnyTls v-if="type == inTypes.AnyTls" :data="inData.out_json" direction="out_json" />
|
||||
<Naive v-if="type == inTypes.Naive" :data="inData.out_json" direction="out_json" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes } from '@/types/inbounds'
|
||||
import Network from './Network.vue'
|
||||
import UoT from './UoT.vue'
|
||||
import Headers from './Headers.vue'
|
||||
import AnyTls from './protocols/AnyTls.vue'
|
||||
import Naive from './protocols/Naive.vue'
|
||||
|
||||
export default {
|
||||
props: ['inData', 'type'],
|
||||
data() {
|
||||
return {
|
||||
inTypes: InTypes,
|
||||
vmessSecurities: [
|
||||
"auto",
|
||||
"none",
|
||||
"zero",
|
||||
"aes-128-gcm",
|
||||
"aes-128-ctr",
|
||||
"chacha20-poly1305",
|
||||
],
|
||||
haveNetwork: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.Shadowsocks,
|
||||
InTypes.VMess,
|
||||
InTypes.Trojan,
|
||||
InTypes.Hysteria,
|
||||
InTypes.VLESS,
|
||||
InTypes.TUIC,
|
||||
InTypes.Hysteria2,
|
||||
],
|
||||
havUoT: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.Shadowsocks,
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
|
||||
needUot():boolean { return this.havUoT.includes(this.$props.type) },
|
||||
packet_encoding: {
|
||||
get() { return this.$props.inData.out_json.packet_encoding != undefined ? this.$props.inData.out_json.packet_encoding : 'none' },
|
||||
set(v:string) { this.$props.inData.out_json.packet_encoding = v != "none" ? v : undefined }
|
||||
},
|
||||
server_ports: {
|
||||
get() { return this.$props.inData.out_json.server_ports?.join(',')?? [] },
|
||||
set(v:string) { this.$props.inData.out_json.server_ports = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
hop_interval: {
|
||||
get() { return this.$props.inData.out_json.hop_interval? parseInt(this.$props.inData.out_json.hop_interval.replace('s','')) : 0 },
|
||||
set(v:number) { this.$props.inData.out_json.hop_interval = v>0 ? v + 's' : undefined }
|
||||
},
|
||||
},
|
||||
components: { Network, UoT, Headers, AnyTls, Naive }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<ExpTextarea
|
||||
v-model="expTextarea.visible"
|
||||
:visible="expTextarea.visible"
|
||||
:label="expTextarea.title"
|
||||
:content="expTextarea.content"
|
||||
@update="saveExpTextarea"
|
||||
@close="closeExpTextarea"
|
||||
/>
|
||||
<v-card style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" v-if="optionInbound">
|
||||
<v-combobox
|
||||
v-model="rule.inbound"
|
||||
:items="inTags"
|
||||
:label="$t('pages.inbounds')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" v-if="optionClient">
|
||||
<v-combobox
|
||||
v-model="rule.auth_user"
|
||||
:items="clients"
|
||||
:label="$t('pages.clients')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIPver">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('rule.ipVer')"
|
||||
:items="[4,6]"
|
||||
v-model.number="rule.ip_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionNetwork">
|
||||
<v-select
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
:label="$t('network')"
|
||||
:items="['tcp','udp','icmp']"
|
||||
v-model="rule.network">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="optionProtocol">
|
||||
<v-select
|
||||
v-model="rule.protocol"
|
||||
:items="protocols"
|
||||
:label="$t('protocol')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionDomain">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="domainKeys"
|
||||
@update:model-value="updateDomainOption($event)"
|
||||
v-model="domainOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain != undefined">
|
||||
<v-textarea :label="$t('rule.domain')"
|
||||
hide-details
|
||||
v-model="domain"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.domain'), 'domain')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain_suffix != undefined">
|
||||
<v-textarea :label="$t('rule.domainSufix')"
|
||||
hide-details
|
||||
v-model="domain_suffix"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.domainSufix'), 'domain_suffix')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain_keyword != undefined">
|
||||
<v-textarea :label="$t('rule.domainKw')"
|
||||
hide-details
|
||||
v-model="domain_keyword"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.domainKw'), 'domain_keyword')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.domain_regex != undefined">
|
||||
<v-textarea :label="$t('rule.domainRgx')"
|
||||
hide-details
|
||||
v-model="domain_regex"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.domainRgx'), 'domain_regex')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.ip_cidr != undefined">
|
||||
<v-textarea :label="$t('rule.ip')"
|
||||
hide-details
|
||||
v-model="ip_cidr"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.ip'), 'ip_cidr')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.ip_is_private != undefined">
|
||||
<v-switch v-model="rule.ip_is_private" color="primary" :label="$t('rule.privateIp')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionPort">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="portKeys"
|
||||
@update:model-value="updatePortOption($event)"
|
||||
v-model="portOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.port != undefined">
|
||||
<v-textarea :label="$t('rule.port')"
|
||||
hide-details
|
||||
v-model="port"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.port'), 'port')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.port_range != undefined">
|
||||
<v-textarea :label="$t('rule.portRange')"
|
||||
hide-details
|
||||
v-model="port_range"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.portRange'), 'port_range')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionSrcIP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="srcIPKeys"
|
||||
@update:model-value="updateSrcIPOption($event)"
|
||||
v-model="srcIPOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined">
|
||||
<v-textarea :label="$t('rule.srcCidr')"
|
||||
hide-details
|
||||
v-model="source_ip_cidr"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.srcCidr'), 'source_ip_cidr')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.source_ip_is_private != undefined">
|
||||
<v-switch v-model="rule.source_ip_is_private" color="primary" :label="$t('rule.srcPrivateIp')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionSrcPort">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="srcPortKeys"
|
||||
@update:model-value="updateSrcPortOption($event)"
|
||||
v-model="srcPortOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.source_port != undefined">
|
||||
<v-textarea :label="$t('rule.srcPort')"
|
||||
hide-details
|
||||
v-model="source_port"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.srcPort'), 'source_port')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.source_port_range != undefined">
|
||||
<v-textarea :label="$t('rule.srcPortRange')"
|
||||
hide-details
|
||||
v-model="source_port_range"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.srcPortRange'), 'source_port_range')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionPreferredBy">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-combobox
|
||||
v-model="rule.preferred_by"
|
||||
:items="outTags || inTags"
|
||||
:label="$t('rule.preferredBy')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionInterface">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="interfaceKeys"
|
||||
@update:model-value="updateInterfaceOption($event)"
|
||||
v-model="interfaceOption">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" v-if="rule.interface_address != undefined || rule.network_interface_address != undefined || rule.default_interface_address != undefined">
|
||||
<v-textarea :label="$t('rule.interfaceAddr')"
|
||||
hide-details
|
||||
v-model="interface_addr"
|
||||
rows="5"
|
||||
no-resize
|
||||
density="compact"
|
||||
append-icon="mdi-arrow-expand"
|
||||
@click:append="openExpTextarea($t('rule.interfaceAddr'), 'interface_address')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionRuleSet">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-combobox
|
||||
v-model="rule.rule_set"
|
||||
:items="rsTags"
|
||||
:label="$t('rule.ruleset')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-switch v-model="rule.rule_set_ip_cidr_match_source" color="primary" :label="$t('rule.rulesetMatchSrc')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('rule.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionInbound" color="primary" :label="$t('pages.inbounds')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionClient" color="primary" :label="$t('pages.clients')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionIPver" color="primary" :label="$t('rule.ipVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionNetwork" color="primary" :label="$t('network')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionProtocol" color="primary" :label="$t('protocol')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDomain" color="primary" :label="$t('rule.domainRules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionPort" color="primary" :label="$t('in.port')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSrcIP" color="primary" :label="$t('rule.srcIpRules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSrcPort" color="primary" :label="$t('rule.srcPortRules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionPreferredBy" color="primary" :label="$t('rule.preferredBy')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionInterface" color="primary" :label="$t('rule.interfaceAddr')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRuleSet" color="primary" :label="$t('rule.ruleset')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ExpTextarea from '@/components/ExpTextarea.vue'
|
||||
export default {
|
||||
components: { ExpTextarea },
|
||||
props: ['rule', 'clients', 'inTags', 'outTags', 'rsTags', 'deleteable'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
domainKeys: ['domain', 'domain_suffix', 'domain_keyword', 'domain_regex', 'ip_cidr', 'ip_is_private'],
|
||||
interfaceKeys: ['interface_address', 'network_interface_address', 'default_interface_address'],
|
||||
portKeys: ['port', 'port_range'],
|
||||
srcIPKeys: ['source_ip_cidr', 'source_ip_is_private'],
|
||||
srcPortKeys: ['source_port', 'source_port_range'],
|
||||
domainOption: 'domain',
|
||||
interfaceOption: 'interface_address',
|
||||
portOption: 'port',
|
||||
srcIPOption: 'source_ip_cidr',
|
||||
srcPortOption: 'source_port',
|
||||
protocols: [
|
||||
{ title: 'HTTP', value: 'http' },
|
||||
{ title: 'TLS', value: 'tls' },
|
||||
{ title: 'QUIC', value: 'quic' },
|
||||
{ title: 'STUN', value: 'stun' },
|
||||
{ title: 'DNS', value: 'dns' },
|
||||
{ title: 'BitTorrent', value: 'bittorrent' },
|
||||
{ title: 'DTLS', value: 'dtls' },
|
||||
{ title: 'SSH', value: 'ssh' },
|
||||
{ title: 'RDP', value: 'rdp' },
|
||||
{ title: 'NTP', value: 'ntp' },
|
||||
],
|
||||
expTextarea: {
|
||||
visible: false,
|
||||
title: '',
|
||||
content: '',
|
||||
object: '',
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateDomainOption(option:string) {
|
||||
this.domainKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = option == 'ip_is_private' ? false : []
|
||||
},
|
||||
updatePortOption(option:string) {
|
||||
this.portKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = []
|
||||
},
|
||||
updateSrcIPOption(option:string) {
|
||||
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = option == 'source_ip_is_private' ? false : []
|
||||
},
|
||||
updateSrcPortOption(option:string) {
|
||||
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = []
|
||||
},
|
||||
updateInterfaceOption(option:string) {
|
||||
this.interfaceKeys.forEach(k => delete this.$props.rule[k])
|
||||
this.$props.rule[option] = []
|
||||
},
|
||||
openExpTextarea(title:string, object:string) {
|
||||
this.expTextarea.visible = !this.expTextarea.visible
|
||||
this.expTextarea.title = title
|
||||
this.expTextarea.content = this.$props.rule[object]?.join('\n') ?? ''
|
||||
this.expTextarea.object = object
|
||||
},
|
||||
saveExpTextarea(results:string[]) {
|
||||
this.$props.rule[this.expTextarea.object] = results
|
||||
this.closeExpTextarea()
|
||||
},
|
||||
closeExpTextarea() {
|
||||
this.expTextarea.visible = false
|
||||
this.expTextarea.title = ''
|
||||
this.expTextarea.object = ''
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
optionInbound: {
|
||||
get() { return this.$props.rule.inbound != undefined },
|
||||
set(v:boolean) { this.$props.rule.inbound = v ? [] : undefined }
|
||||
},
|
||||
optionClient: {
|
||||
get() { return this.$props.rule.auth_user != undefined },
|
||||
set(v:boolean) { this.$props.rule.auth_user = v ? [] : undefined }
|
||||
},
|
||||
optionIPver: {
|
||||
get() { return this.$props.rule.ip_version != undefined },
|
||||
set(v:boolean) { this.$props.rule.ip_version = v ? 4 : undefined }
|
||||
},
|
||||
optionProtocol: {
|
||||
get() { return this.$props.rule.protocol != undefined },
|
||||
set(v:boolean) { this.$props.rule.protocol = v ? ['http'] : undefined }
|
||||
},
|
||||
optionDomain: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.domainKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.domain = []
|
||||
} else {
|
||||
this.domainKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.domainOption = 'domain'
|
||||
}
|
||||
},
|
||||
optionPort: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.portKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.port = []
|
||||
} else {
|
||||
this.portKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.portOption = 'port'
|
||||
}
|
||||
},
|
||||
optionSrcIP: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.srcIPKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.source_ip_cidr = []
|
||||
} else {
|
||||
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.srcIPOption = 'source_ip_cidr'
|
||||
}
|
||||
},
|
||||
optionSrcPort: {
|
||||
get() { return Object.keys(this.$props.rule).some(r => this.srcPortKeys.includes(r)) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.source_port = []
|
||||
} else {
|
||||
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.srcPortOption = 'source_port'
|
||||
}
|
||||
},
|
||||
optionPreferredBy: {
|
||||
get() { return this.$props.rule.preferred_by != undefined },
|
||||
set(v:boolean) { this.$props.rule.preferred_by = v ? [] : undefined }
|
||||
},
|
||||
optionInterface: {
|
||||
get() { return this.interfaceKeys.some(k => this.$props.rule[k] != undefined) },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.interface_address = []
|
||||
} else {
|
||||
this.interfaceKeys.forEach(k => delete this.$props.rule[k])
|
||||
}
|
||||
this.interfaceOption = 'interface_address'
|
||||
}
|
||||
},
|
||||
optionRuleSet: {
|
||||
get() { return this.$props.rule.rule_set != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.rule.rule_set = []
|
||||
this.$props.rule.rule_set_ip_cidr_match_source = false
|
||||
} else {
|
||||
delete this.$props.rule.rule_set
|
||||
delete this.$props.rule.rule_set_ip_cidr_match_source
|
||||
}
|
||||
}
|
||||
},
|
||||
optionNetwork: {
|
||||
get() { return this.$props.rule.network != undefined },
|
||||
set(v:boolean) { this.$props.rule.network = v ? [] : undefined }
|
||||
},
|
||||
domain: {
|
||||
get() { return this.$props.rule.domain?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.domain = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
domain_suffix: {
|
||||
get() { return this.$props.rule.domain_suffix?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.domain_suffix = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
domain_keyword: {
|
||||
get() { return this.$props.rule.domain_keyword?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.domain_keyword = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
domain_regex: {
|
||||
get() { return this.$props.rule.domain_regex?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.domain_regex = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
ip_cidr: {
|
||||
get() { return this.$props.rule.ip_cidr?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.ip_cidr = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
port: {
|
||||
get() { return this.$props.rule.port?.join('\n') ?? '' },
|
||||
set(v:string) {
|
||||
const lines = v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0)
|
||||
if (!v.endsWith('\n')) {
|
||||
this.$props.rule.port = lines.length > 0 ? lines.map((str:string) => parseInt(str, 10)).filter((n:number) => !isNaN(n)) : []
|
||||
}
|
||||
}
|
||||
},
|
||||
port_range: {
|
||||
get() { return this.$props.rule.port_range?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.port_range = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
source_ip_cidr: {
|
||||
get() { return this.$props.rule.source_ip_cidr?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.source_ip_cidr = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
source_port: {
|
||||
get() { return this.$props.rule.source_port?.join('\n') ?? '' },
|
||||
set(v:string) {
|
||||
const lines = v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0)
|
||||
if (!v.endsWith('\n')) {
|
||||
this.$props.rule.source_port = lines.length > 0 ? lines.map((str:string) => parseInt(str, 10)).filter((n:number) => !isNaN(n)) : []
|
||||
}
|
||||
}
|
||||
},
|
||||
source_port_range: {
|
||||
get() { return this.$props.rule.source_port_range?.join('\n') ?? '' },
|
||||
set(v:string) { this.$props.rule.source_port_range = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : [] }
|
||||
},
|
||||
interface_addr: {
|
||||
get() {
|
||||
const k = this.interfaceKeys.find(k => this.$props.rule[k] != undefined)
|
||||
return k ? this.$props.rule[k]?.join('\n') ?? '' : ''
|
||||
},
|
||||
set(v:string) {
|
||||
const k = this.interfaceKeys.find(k => this.$props.rule[k] != undefined)
|
||||
if (k) this.$props.rule[k] = v.length > 0 ? v.split('\n').map((s:string) => s.trim()).filter((s:string) => s.length > 0) : []
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const ruleKeys = Object.keys(this.$props.rule)
|
||||
if (this.optionDomain) {
|
||||
const enabledOption = this.domainKeys.filter(k => ruleKeys.includes(k))
|
||||
this.domainOption = enabledOption.length>0 ? enabledOption[0] : 'domain'
|
||||
}
|
||||
if (this.optionPort) {
|
||||
const enabledOption = this.portKeys.filter(k => ruleKeys.includes(k))
|
||||
this.portOption = enabledOption.length>0 ? enabledOption[0] : 'port'
|
||||
}
|
||||
if (this.optionSrcIP) {
|
||||
const enabledOption = this.srcIPKeys.filter(k => ruleKeys.includes(k))
|
||||
this.srcIPOption = enabledOption.length>0 ? enabledOption[0] : 'source_ip_cidr'
|
||||
}
|
||||
if (this.optionSrcPort) {
|
||||
const enabledOption = this.srcPortKeys.filter(k => ruleKeys.includes(k))
|
||||
this.srcPortOption = enabledOption.length>0 ? enabledOption[0] : 'source_port'
|
||||
}
|
||||
if (this.optionInterface) {
|
||||
const enabledOption = this.interfaceKeys.filter(k => ruleKeys.includes(k))
|
||||
this.interfaceOption = enabledOption.length>0 ? enabledOption[0] : 'interface_address'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<v-row density="compact">
|
||||
<v-col cols="12" class="v-card-subtitle" style="margin-top: -5px;">{{ label }}</v-col>
|
||||
<v-col :cols="data.type == 'local' ? 12 : 4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="['udp','tcp','local','tls','quic','h3']"
|
||||
@update:model-value="updateType($event)"
|
||||
density="compact"
|
||||
:class="data.type != 'local' ? 'noGutters' : ''"
|
||||
v-model="data.type">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="5" v-if="data.type != 'local'">
|
||||
<v-text-field
|
||||
v-model="data.server"
|
||||
:label="$t('in.addr')"
|
||||
density="compact"
|
||||
class="noGutters"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="3" v-if="data.type != 'local'">
|
||||
<v-text-field
|
||||
v-model.number="data.server_port"
|
||||
:label="$t('in.port')"
|
||||
density="compact"
|
||||
type="number"
|
||||
class="noGutters"
|
||||
min="1"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data', 'label'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
updateType(t:string) {
|
||||
if (t == 'local') {
|
||||
delete this.data.server
|
||||
delete this.data.server_port
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.noGutters .v-field__input,
|
||||
.noGutters .v-field {
|
||||
text-align: center !important;
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<Editor
|
||||
v-model="enableEditor"
|
||||
:data="settings.subClashExt"
|
||||
:visible="enableEditor"
|
||||
:title="$t('editor') + ' - ' + $t('setting.clashSub')"
|
||||
@close="enableEditor = false"
|
||||
@save="saveEditor"
|
||||
/>
|
||||
<v-card>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="optionMixed">
|
||||
<v-text-field type="number" v-model.number="mixedPort" min="1" max="65535" :label="$t('setting.mixedPort')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="optionMixed">
|
||||
<v-switch color="primary" v-model="allowLan" :label="$t('types.ts.allowLanAccess')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="optionExt">
|
||||
<v-text-field v-model="externalController" :label="$t('basic.exp.extController')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="optionLog">
|
||||
<v-select v-model="logLevel" :items="['debug', 'info', 'warning', 'error']" :label="$t('basic.log.title') + ' - ' + $t('basic.log.level')" hide-details></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="optionTun">
|
||||
<v-switch color="primary" v-model="tun" :label="$t('setting.tun')" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="optionDns">
|
||||
<v-switch color="primary" v-model="dns" :label="$t('pages.dns')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionRules">
|
||||
<v-col cols="12" sm="12" md="6" lg="4">
|
||||
<v-select
|
||||
v-model="rules"
|
||||
:items="rulesIP"
|
||||
chips
|
||||
closable-chips
|
||||
multiple
|
||||
hide-details
|
||||
:label="$t('pages.rules')"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="openEditor" variant="outlined" hide-details>{{ $t('editor') }}</v-btn>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('setting.jsonSubOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMixed" color="primary" :label="$t('setting.mixedPort')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTun" color="primary" :label="$t('setting.tun')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionExt" color="primary" :label="$t('basic.exp.extController')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionLog" color="primary" :label="$t('basic.log.title')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDns" color="primary" :label="$t('pages.dns')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRules" color="primary" :label="$t('pages.rules')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { push } from 'notivue'
|
||||
import Editor from './Editor.vue'
|
||||
import yaml from 'yaml'
|
||||
import { i18n } from '@/locales'
|
||||
export default {
|
||||
props: ['settings'],
|
||||
data() {
|
||||
return {
|
||||
enableEditor: false,
|
||||
menu: false,
|
||||
defaultConfig: {
|
||||
"mixed-port": 7890,
|
||||
"allow-lan": false,
|
||||
"mode": "rule",
|
||||
"log-level": "info",
|
||||
"external-controller": "127.0.0.1:9090",
|
||||
"tun": {
|
||||
"enable": true,
|
||||
"stack": "system",
|
||||
"auto-route": true,
|
||||
"auto-detect-interface": true,
|
||||
"dns-hijack": ["any:53"],
|
||||
},
|
||||
"dns": {
|
||||
"enable": true,
|
||||
"ipv6": false,
|
||||
"enhanced-mode": "fake-ip",
|
||||
"fake-ip-range": "198.18.0.1/16",
|
||||
"default-nameserver": ["8.8.8.8","1.1.1.1"],
|
||||
"nameserver": [
|
||||
"https://doh.pub/dns-query",
|
||||
"https://1.0.0.1/dns-query"
|
||||
],
|
||||
"fallback": ["tcp://9.9.9.9:53"],
|
||||
"fake-ip-filter": ["*.lan", "localhost", "*.local"]
|
||||
},
|
||||
"rules": [
|
||||
"GEOIP,Private,DIRECT",
|
||||
"MATCH,Proxy"
|
||||
]
|
||||
},
|
||||
rulesIP: [
|
||||
{ title: 'Private-Direct', value: 'GEOIP,Private,DIRECT' },
|
||||
{ title: 'Private-Block', value: 'GEOIP,Private,REJECT' },
|
||||
{ title: 'LAN-Direct', value: 'GEOIP,LAN,DIRECT' },
|
||||
{ title: 'LAN-Block', value: 'GEOIP,LAN,REJECT' },
|
||||
{ title: 'Ads-Direct', value: 'GEOIP,Ads,DIRECT' },
|
||||
{ title: 'Ads-Block', value: 'GEOIP,Ads,REJECT' },
|
||||
{ title: '🇨🇳 China-Direct', value: 'GEOIP,CN,DIRECT' },
|
||||
{ title: '🇨🇳 China-Block', value: 'GEOIP,CN,REJECT' },
|
||||
{ title: '🇮🇷 Iran-Direct', value: 'GEOIP,CATEGORY-IR,DIRECT' },
|
||||
{ title: '🇮🇷 Iran-Block', value: 'GEOIP,CATEGORY-IR,REJECT' },
|
||||
{ title: '🇻🇳 Vietnam-Direct', value: 'GEOIP,CATEGORY-VN,DIRECT' },
|
||||
{ title: '🇻🇳 Vietnam-Block', value: 'GEOIP,CATEGORY-VN,REJECT' },
|
||||
{ title: '🇯🇵 Japan-Direct', value: 'GEOIP,JP,DIRECT' },
|
||||
{ title: '🇯🇵 Japan-Block', value: 'GEOIP,JP,REJECT' },
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openEditor() {
|
||||
this.enableEditor = true
|
||||
},
|
||||
saveEditor(data:string) {
|
||||
try {
|
||||
const result = yaml.parse(data)
|
||||
if (typeof result != 'object' || Array.isArray(result)) throw new Error()
|
||||
} catch (e) {
|
||||
push.error({
|
||||
message: i18n.global.t('failed') + ": " + i18n.global.t('error.invalidData'),
|
||||
duration: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$props.settings.subClashExt = data
|
||||
this.enableEditor = false
|
||||
},
|
||||
updateMetaJson(data:any, key:string) {
|
||||
let newMetaJson = this.metaJson
|
||||
if (data==null) {
|
||||
delete newMetaJson[key]
|
||||
} else {
|
||||
newMetaJson[key] = data
|
||||
}
|
||||
this.metaJson = newMetaJson
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
metaJson: {
|
||||
get() {
|
||||
try {
|
||||
return yaml.parse(this.settings.subClashExt)??{}
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
set(v:any) {
|
||||
this.settings.subClashExt = Object.keys(v).length==0 ? "" : yaml.stringify(v)
|
||||
}
|
||||
},
|
||||
optionMixed: {
|
||||
get() { return this.metaJson['mixed-port']>0 },
|
||||
set(v:boolean) {
|
||||
this.updateMetaJson(v ? this.defaultConfig['mixed-port'] : null, 'mixed-port')
|
||||
this.updateMetaJson(v ? this.defaultConfig['allow-lan'] : null, 'allow-lan')
|
||||
}
|
||||
},
|
||||
optionTun: {
|
||||
get() { return this.metaJson['tun']?.['enable']?? false },
|
||||
set(v:boolean) { this.updateMetaJson(v ? this.defaultConfig['tun'] : null, 'tun') }
|
||||
},
|
||||
optionExt: {
|
||||
get() { return this.metaJson['external-controller']?.length>0 },
|
||||
set(v:boolean) { this.updateMetaJson(v ? this.defaultConfig['external-controller'] : null, 'external-controller') }
|
||||
},
|
||||
optionLog: {
|
||||
get() { return this.metaJson['log-level']?.length>0 },
|
||||
set(v:boolean) { this.updateMetaJson(v ? this.defaultConfig['log-level'] : null, 'log-level') }
|
||||
},
|
||||
optionDns: {
|
||||
get() { return this.metaJson['dns']?.['enable']?? false },
|
||||
set(v:boolean) { this.updateMetaJson(v ? this.defaultConfig['dns'] : null, 'dns') }
|
||||
},
|
||||
optionRules: {
|
||||
get() { return this.metaJson['rules']?.length>0 },
|
||||
set(v:boolean) {
|
||||
this.updateMetaJson(v ? this.defaultConfig['rules'] : null, 'rules')
|
||||
this.updateMetaJson(v ? this.defaultConfig['mode'] : null, 'mode')
|
||||
}
|
||||
},
|
||||
mixedPort: {
|
||||
get() { return this.metaJson['mixed-port'] },
|
||||
set(v:number) { this.updateMetaJson(v, 'mixed-port') }
|
||||
},
|
||||
allowLan: {
|
||||
get() { return this.metaJson['allow-lan'] },
|
||||
set(v:boolean) { this.updateMetaJson(v, 'allow-lan') }
|
||||
},
|
||||
externalController: {
|
||||
get() { return this.metaJson['external-controller'] },
|
||||
set(v:string) { this.updateMetaJson(v, 'external-controller') }
|
||||
},
|
||||
logLevel: {
|
||||
get() { return this.metaJson['log-level'] },
|
||||
set(v:string) { this.updateMetaJson(v, 'log-level') }
|
||||
},
|
||||
dns: {
|
||||
get() { return this.metaJson['dns']?.['enable'] ?? false },
|
||||
set(v:boolean) { this.updateMetaJson({ ...this.metaJson['dns'], 'enable': v }, 'dns') }
|
||||
},
|
||||
tun: {
|
||||
get() { return this.metaJson['tun']?.['enable'] ?? false },
|
||||
set(v:boolean) { this.updateMetaJson({ ...this.metaJson['tun'], 'enable': v }, 'tun') }
|
||||
},
|
||||
rules: {
|
||||
get() { return this.metaJson.rules.length > 0 ? this.metaJson.rules.filter((r:string) => r != "MATCH,Proxy") : [] },
|
||||
set(v:string[]) {
|
||||
let newRules = <string[]>[]
|
||||
v.forEach((r:string) => { newRules.push(r) })
|
||||
this.updateMetaJson([ ...newRules, "MATCH,Proxy" ], 'rules')
|
||||
}
|
||||
}
|
||||
},
|
||||
components: { Editor }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<Editor
|
||||
v-model="enableEditor"
|
||||
:data="settings.subJsonExt"
|
||||
:visible="enableEditor"
|
||||
:title="$t('editor') + ' - ' + $t('setting.jsonSub')"
|
||||
@close="enableEditor = false"
|
||||
@save="saveEditor"
|
||||
/>
|
||||
<v-card>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
v-model="ruleToDirect"
|
||||
:items="geoList"
|
||||
:label="$t('setting.toDirect')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
v-model="ruleToBlock"
|
||||
:items="geoList"
|
||||
:label="$t('setting.toBlock')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="enableLog">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.log.level')"
|
||||
:items="levels"
|
||||
v-model="subJsonExt.log.level">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="subJsonExt.log.timestamp" color="primary" :label="$t('setting.timestamp')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="enableDns">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('dns.final')"
|
||||
:items="dnsTags"
|
||||
v-model="subJsonExt.dns.final">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<SimpleDNS :data="proxyDns" :label="$t('setting.globalDns')" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<SimpleDNS :data="directDns" :label="$t('setting.directDns')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="enableDns">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.routing.defaultDns')"
|
||||
:items="dnsTags"
|
||||
clearable
|
||||
@click:clear="delete subJsonExt.default_domain_resolver"
|
||||
v-model="subJsonExt.default_domain_resolver">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
v-model="dnsToDirect"
|
||||
:items="geositeList"
|
||||
:label="$t('setting.toDirectDns')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="enableInb">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-combobox
|
||||
v-model="inbounds[0].address"
|
||||
:items="defaultInb[0].address"
|
||||
chips
|
||||
multiple
|
||||
hide-details
|
||||
:label="$t('in.addr')"
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="inbounds[0].mtu"
|
||||
hide-details
|
||||
label="MTU"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-combobox
|
||||
v-model="inbounds[0].exclude_package"
|
||||
:items="['ir.mci.ecareapp','com.myirancell']"
|
||||
chips
|
||||
multiple
|
||||
hide-details
|
||||
:label="$t('setting.excludePkg')"
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch
|
||||
v-model="platformProxy"
|
||||
hide-details
|
||||
color="primary"
|
||||
label="Platform HTTP proxy"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="openEditor" variant="outlined" hide-details>{{ $t('editor') }}</v-btn>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('setting.jsonSubOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableLog" color="primary" :label="$t('basic.log.title')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableDns" color="primary" label="DNS" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableInb" color="primary" :label="$t('objects.inbound')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableExp" color="primary" label="Experimental" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Editor from './Editor.vue'
|
||||
import SimpleDNS from './SimpleDNS.vue'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
export default {
|
||||
props: ['settings'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
enableEditor: false,
|
||||
subJsonExt: <any>{},
|
||||
levels: ["trace", "debug", "info", "warn", "error", "fatal", "panic"],
|
||||
defaultLog: {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
defaultInb: [
|
||||
{
|
||||
"type": "tun",
|
||||
"address": [
|
||||
"172.19.0.1/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"mtu": 9000,
|
||||
"auto_route": true,
|
||||
"strict_route": false,
|
||||
"endpoint_independent_nat": false,
|
||||
"stack": "mixed",
|
||||
"exclude_package": [],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": true,
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 2080
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2080,
|
||||
"users": []
|
||||
}
|
||||
],
|
||||
defaultExp: {
|
||||
"clash_api": {
|
||||
"external_controller": "127.0.0.1:9090",
|
||||
"external_ui": "ui",
|
||||
"secret": "",
|
||||
"external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
|
||||
"external_ui_download_detour": "direct",
|
||||
"default_mode": "rule"
|
||||
},
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_fakeip": false
|
||||
}
|
||||
},
|
||||
defaultDns: {
|
||||
"servers": [
|
||||
{
|
||||
"type": "tcp",
|
||||
"tag": "proxy-dns",
|
||||
"server": "8.8.8.8",
|
||||
"server_port": 53,
|
||||
"detour": "proxy",
|
||||
"domain_resolver": "local-dns",
|
||||
},
|
||||
{
|
||||
"tag": "direct-dns",
|
||||
"type": "local",
|
||||
},
|
||||
{
|
||||
"tag": "local-dns",
|
||||
"type": "local",
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"source_ip_cidr": [
|
||||
"172.19.0.0/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"action": "route",
|
||||
"server": "proxy-dns"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Direct",
|
||||
"action": "route",
|
||||
"server": "direct-dns"
|
||||
},
|
||||
{
|
||||
"source_ip_cidr": [
|
||||
"172.19.0.0/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"action": "route",
|
||||
"server": "proxy-dns"
|
||||
},
|
||||
],
|
||||
"final": "local-dns",
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
geositeList: [
|
||||
{ title: "Private", value: "geosite-private" },
|
||||
{ title: "Ads", value: "geosite-ads" },
|
||||
{ title: "🇮🇷 Iran", value: "geosite-ir" },
|
||||
{ title: "🇨🇳 China", value: "geosite-cn" },
|
||||
{ title: "🇻🇳 Vietnam", value: "geosite-vn" },
|
||||
],
|
||||
geoList: [
|
||||
{ title: "Site-Private", value: "geoip-private" },
|
||||
{ title: "IP-Private", value: "geosite-private" },
|
||||
{ title: "Site-Ads", value: "geosite-ads" },
|
||||
{ title: "🇮🇷 Site-Iran", value: "geosite-ir" },
|
||||
{ title: "🇮🇷 IP-Iran", value: "geoip-ir" },
|
||||
{ title: "🇨🇳 Site-China", value: "geosite-cn" },
|
||||
{ title: "🇨🇳 IP-China", value: "geoip-cn" },
|
||||
{ title: "🇻🇳 Site-Vietnam", value: "geosite-vn" },
|
||||
{ title: "🇻🇳 IP-Vietnam", value: "geoip-vn" },
|
||||
],
|
||||
geo: [
|
||||
{
|
||||
tag: "geosite-ads",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-private",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-ir",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ir.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-cn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-vn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://github.com/Thaomtam/Geosite-vn/raw/rule-set/Geosite-vn.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-private",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/private.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-ir",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/ir.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-cn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-vn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/vn.srs",
|
||||
download_detour: "direct"
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enableLog: {
|
||||
get() :boolean { return this.subJsonExt?.log != undefined },
|
||||
set(v:boolean) { v ? this.subJsonExt.log = this.defaultLog : delete this.subJsonExt.log }
|
||||
},
|
||||
enableDns: {
|
||||
get() :boolean { return this.subJsonExt?.dns != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.subJsonExt.dns = this.defaultDns
|
||||
if (this.rules == undefined) this.subJsonExt.rules = [{ action: 'sniff' }]
|
||||
this.subJsonExt.rules.unshift({ protocol: "dns", action: "hijack-dns" })
|
||||
} else {
|
||||
delete this.subJsonExt.dns
|
||||
const rules = this.subJsonExt?.rules?.filter((r:any) => r.protocol != "dns") ?? []
|
||||
if (rules.length >= 0) this.subJsonExt.rules = rules
|
||||
if (this.rules.length == 0) delete this.subJsonExt.rules
|
||||
}
|
||||
}
|
||||
},
|
||||
enableInb: {
|
||||
get() :boolean { return this.subJsonExt?.inbounds != undefined },
|
||||
set(v:boolean) { v ? this.subJsonExt.inbounds = this.defaultInb.slice() : delete this.subJsonExt.inbounds }
|
||||
},
|
||||
enableExp: {
|
||||
get() :boolean { return this.subJsonExt?.experimental != undefined },
|
||||
set(v:boolean) { v ? this.subJsonExt.experimental = this.defaultExp : delete this.subJsonExt.experimental }
|
||||
},
|
||||
dns():any { return this.subJsonExt?.dns?? undefined },
|
||||
proxyDns: {
|
||||
get() :any { return this.dns?.servers?.findLast((d:any) => d.tag == "proxy-dns")?? {} },
|
||||
set(v:any) {
|
||||
let sIndex = this.dns.servers.findIndex((d:any) => d.tag == "proxy-dns")
|
||||
if (sIndex === -1 || sIndex == undefined) {
|
||||
this.dns.servers.push({ ...this.defaultDns.servers[0], ...v })
|
||||
} else {
|
||||
this.dns.servers[sIndex] = { ...this.defaultDns.servers[0], ...v }
|
||||
}
|
||||
}
|
||||
},
|
||||
directDns: {
|
||||
get() :any { return this.dns?.servers?.findLast((d:any) => d.tag == "direct-dns")?? {} },
|
||||
set(v:any) {
|
||||
const sIndex = this.dns.servers.findIndex((d:any) => d.tag == "direct-dns")
|
||||
if (sIndex === -1 || sIndex == undefined) {
|
||||
this.dns.servers.push({ ...this.defaultDns.servers[1], ...v })
|
||||
} else {
|
||||
this.dns.servers[sIndex] = { ...this.defaultDns.servers[1], ...v }
|
||||
}
|
||||
},
|
||||
},
|
||||
dnsTags() { return this.dns?.servers?.map((d:any) => d.tag) ?? [] },
|
||||
final: {
|
||||
get() :string { return this.dns.final?? "" },
|
||||
set(v:string) { this.dns.final = v.length>0 ? v : undefined }
|
||||
},
|
||||
dnsToDirect: {
|
||||
get() :string[] {
|
||||
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
|
||||
return ruleIndex >= 0 ? this.dns.rules[ruleIndex].rule_set : []
|
||||
},
|
||||
set(v:string[]) {
|
||||
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
|
||||
if (v.length>0) {
|
||||
if (ruleIndex >= 0){
|
||||
this.dns.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
this.dns.rules.push({ rule_set: v, action: "route", server: "direct-dns" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1)
|
||||
}
|
||||
this.updateRuleSets()
|
||||
}
|
||||
},
|
||||
inbounds():any[] { return this.subJsonExt?.inbounds?? undefined },
|
||||
platformProxy: {
|
||||
get() :boolean { return this.inbounds[0]?.platform != undefined },
|
||||
set(v:boolean) { this.subJsonExt.inbounds[0].platform = v ? this.defaultInb[0].platform : undefined }
|
||||
},
|
||||
rules():any { return this.subJsonExt?.rules?? undefined },
|
||||
ruleToDirect: {
|
||||
get() :string[] {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
|
||||
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
|
||||
},
|
||||
set(v:string[]) {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
|
||||
if (v.length>0) {
|
||||
if (ruleIndex >= 0){
|
||||
this.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
if (this.rules == undefined) this.subJsonExt.rules = []
|
||||
this.rules.push({ rule_set: v, action: "route", outbound: "direct" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
|
||||
}
|
||||
this.updateRuleSets()
|
||||
}
|
||||
},
|
||||
ruleToBlock: {
|
||||
get() :string[] {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.action == "reject" && Object.hasOwn(r,'rule_set'))
|
||||
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
|
||||
},
|
||||
set(v:string[]) {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.action == "reject" && Object.hasOwn(r,'rule_set'))
|
||||
if (v.length>0) {
|
||||
if (ruleIndex >= 0){
|
||||
this.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
if (this.rules == undefined) this.subJsonExt.rules = []
|
||||
this.rules.push({ rule_set: v, action: "reject" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
|
||||
}
|
||||
this.updateRuleSets()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
if (this.$props.settings?.subJsonExt?.length>0){
|
||||
this.subJsonExt = JSON.parse(this.$props.settings.subJsonExt)
|
||||
} else {
|
||||
this.subJsonExt = <any>{}
|
||||
}
|
||||
},
|
||||
updateRuleSets(){
|
||||
let tags = <string[]>[]
|
||||
if (this.dns?.rules?.length>0) this.dns.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
|
||||
if (this.rules?.length>0) this.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
|
||||
if (tags.length>0){
|
||||
this.subJsonExt.rule_set = this.geo.filter((g:any) => tags.includes(g.tag))
|
||||
} else {
|
||||
delete this.subJsonExt.rule_set
|
||||
}
|
||||
if (this.rules.length == 0) delete this.subJsonExt.rules
|
||||
},
|
||||
openEditor() {
|
||||
this.enableEditor = true
|
||||
},
|
||||
saveEditor(data:string) {
|
||||
try {
|
||||
this.subJsonExt = JSON.parse(data)
|
||||
} catch (e) {
|
||||
push.error({
|
||||
message: i18n.global.t('failed') + ": " + i18n.global.t('error.invalidData'),
|
||||
duration: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
this.enableEditor = false
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
this.loadData()
|
||||
},
|
||||
watch:{
|
||||
subJsonExt:{
|
||||
handler(v) {
|
||||
this.$props.settings.subJsonExt = Object.keys(v).length>0 ? JSON.stringify(v, null, 2) : ""
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
components: { Editor, SimpleDNS }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.transport')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('transport.enable')" v-model="tpEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tpEnable">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(trspTypes).map((key,index) => ({title: key, value: Object.values(trspTypes)[index]}))"
|
||||
v-model="transportType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Http v-if="Transport.type == trspTypes.HTTP" :transport="Transport" />
|
||||
<WebSocket v-if="Transport.type == trspTypes.WebSocket" :transport="Transport" />
|
||||
<GRPC v-if="Transport.type == trspTypes.gRPC" :transport="Transport" />
|
||||
<HttpUpgrade v-if="Transport.type == trspTypes.HTTPUpgrade" :transport="Transport" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { TrspTypes, Transport } from '@/types/transport'
|
||||
import Http from './transports/Http.vue'
|
||||
import WebSocket from './transports/WebSocket.vue'
|
||||
import GRPC from './transports/gRPC.vue'
|
||||
import HttpUpgrade from './transports/HttpUpgrade.vue'
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
trspTypes: TrspTypes
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Transport() {
|
||||
return <Transport>this.$props.data.transport
|
||||
},
|
||||
tpEnable: {
|
||||
get() { return Object.hasOwn(this.$props.data.transport, 'type') },
|
||||
set(newValue: boolean) { this.$props.data.transport = newValue ? { type: 'http' } : {} }
|
||||
},
|
||||
transportType: {
|
||||
get() { return this.Transport.type },
|
||||
set(newValue: string) { this.$props.data.transport = { type: newValue } }
|
||||
}
|
||||
},
|
||||
components: { Http, WebSocket, GRPC, HttpUpgrade }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<v-select
|
||||
hide-details
|
||||
label="UDP over TCP"
|
||||
:items="versions"
|
||||
v-model="udp_over_tcp">
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
versions: [
|
||||
{ title: this.$t('disable'), value: 0 },
|
||||
{ title: "1", value: 1 },
|
||||
{ title: "2", value: 2 },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
udp_over_tcp: {
|
||||
get():number { return this.$props.data.udp_over_tcp?.version?? 0 },
|
||||
set(v:number) { this.$props.data.udp_over_tcp = v > 0 ? { enabled: true, version: v } : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('pages.clients')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select v-model="data.model" :items="initUsersModels" @update:model-value="data.values = []" hide-details></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.model == 'group'">
|
||||
<v-select v-model="data.values" multiple chips :items="groupNames" :label="$t('client.group')" hide-details></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8" v-if="data.model == 'client'">
|
||||
<v-select v-model="data.values" multiple chips :items="clientNames" :label="$t('pages.clients')" hide-details></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
|
||||
export default {
|
||||
props: ['data', 'clients'],
|
||||
data() {
|
||||
return {
|
||||
initUsersModels: [
|
||||
{ title: i18n.global.t('none'), value: 'none' },
|
||||
{ title: i18n.global.t('all'), value: 'all' },
|
||||
{ title: i18n.global.t('client.group'), value: 'group' },
|
||||
{ title: i18n.global.t('pages.clients'), value: 'client' },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clientNames() {
|
||||
return this.$props.clients.map((c:any) => { return { title: c.name, value: c.id } } )
|
||||
},
|
||||
groupNames() {
|
||||
return Array.from(new Set(this.$props.clients.map((c:any) => c.group)))
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
v-model="privateKey"
|
||||
:label="$t('types.wg.privKey')"
|
||||
append-icon="mdi-key-star"
|
||||
@click:append="refreshKey"
|
||||
hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="publicKey" :label="$t('types.wg.pubKey')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="data.pre_shared_key" :label="$t('types.wg.psk')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
v-model="address">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="KeepAlive"
|
||||
type="number"
|
||||
min="0"
|
||||
:suffix="$t('date.s')"
|
||||
hide-details
|
||||
v-model.number="keepAlive">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="allowed_ips" :label="$t('types.wg.allowedIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="reserved" :label="'Reserved ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data', 'ext'],
|
||||
emits: ['refreshPeerKey'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
refreshKey() {
|
||||
this.$emit('refreshPeerKey')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allowed_ips: {
|
||||
get() { return this.$props.data.allowed_ips?.join(',') },
|
||||
set(v:string) { this.$props.data.allowed_ips = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
reserved: {
|
||||
get() { return this.$props.data.reserved?.join(',') },
|
||||
set(v:string) {
|
||||
if(!v.endsWith(',')) {
|
||||
this.$props.data.reserved = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
address: {
|
||||
get() { return this.$props.data.address },
|
||||
set(v:string) { this.$props.data.address = v.length > 0 ? v : undefined }
|
||||
},
|
||||
port: {
|
||||
get() { return this.$props.data.port },
|
||||
set(v:number) { this.$props.data.port = v > 0 ? v : undefined }
|
||||
},
|
||||
keepAlive: {
|
||||
get() { return this.$props.data.persistent_keepalive_interval?? 0 },
|
||||
set(v:number) { this.$props.data.persistent_keepalive_interval = v > 0 ? v : undefined }
|
||||
},
|
||||
privateKey: {
|
||||
get() {
|
||||
const indexKeys = this.$props.ext?.keys.findIndex((key: any) => key.public_key == this.$props.data.public_key)?? -1
|
||||
return indexKeys > -1 ? this.$props.ext.keys[indexKeys].private_key : ''
|
||||
},
|
||||
set(v:string) {
|
||||
const indexKeys = this.$props.ext?.keys.findIndex((key: any) => key.public_key == this.$props.data.public_key)?? -1
|
||||
this.$props.ext.keys[indexKeys].private_key = v
|
||||
}
|
||||
},
|
||||
publicKey: {
|
||||
get() { return this.$props.data.public_key },
|
||||
set(v:string) {
|
||||
const indexKeys = this.$props.ext?.keys.findIndex((key: any) => key.public_key == this.$props.data.public_key)?? -1
|
||||
this.$props.ext.keys[indexKeys].public_key = v
|
||||
this.$props.data.public_key = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<Notivue v-slot="item">
|
||||
<NotivueSwipe :item="item">
|
||||
<Notification
|
||||
:item="item"
|
||||
:theme="theme"
|
||||
:dir="direction"
|
||||
:icons="outlinedIcons"
|
||||
:hideClose="true"
|
||||
@click="item.clear"
|
||||
/>
|
||||
</NotivueSwipe>
|
||||
</Notivue>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Notivue, Notification, NotivueSwipe, outlinedIcons, pastelTheme, darkTheme } from 'notivue'
|
||||
import { computed } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
const Theme = useTheme()
|
||||
|
||||
const theme = computed(() =>{
|
||||
let currenTheme = Theme.global.name.value == "light" ? pastelTheme : darkTheme
|
||||
currenTheme = {
|
||||
...currenTheme,
|
||||
'--nv-width': 'auto',
|
||||
}
|
||||
return currenTheme
|
||||
})
|
||||
|
||||
const direction = computed(() => {
|
||||
return vuetify.locale.isRtl ? 'rtl' : 'ltr'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--nv-z: 10020;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-subtitle v-if="direction != 'out_json'">AnyTls</v-card-subtitle>
|
||||
<v-row v-if="direction == 'in'">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-textarea
|
||||
label="Padding scheme"
|
||||
auto-grow
|
||||
hide-details
|
||||
v-model="padding_scheme">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="8" v-if="direction == 'out'">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
v-model="data.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.anytls.idleInterval')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
:suffix="$t('date.s')"
|
||||
v-model.number="idleInterval">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.anytls.idleTimeout')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
:suffix="$t('date.s')"
|
||||
v-model.number="idleTimeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.anytls.minIdle')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="minIdle">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data', 'direction'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
padding_scheme: {
|
||||
get() { return this.data.padding_scheme?.length > 0 ? this.data.padding_scheme.join("\n") : '' },
|
||||
set(v:string) { this.data.padding_scheme = v.length > 0 ? v.split("\n") : undefined }
|
||||
},
|
||||
idleInterval: {
|
||||
get() { return this.data.idle_session_check_interval?.length > 0 ? parseInt(this.data.idle_session_check_interval.replace('s','')) : 30 },
|
||||
set(v:number) { this.data.idle_session_check_interval = v && v >= 0 ? `${v}s` : undefined }
|
||||
},
|
||||
idleTimeout: {
|
||||
get() { return this.data.idle_session_timeout?.length > 0 ? parseInt(this.data.idle_session_timeout.replace('s','')) : 30 },
|
||||
set(v:number) { this.data.idle_session_timeout = v && v >= 0 ? `${v}s` : undefined }
|
||||
},
|
||||
minIdle: {
|
||||
get() { return this.data.min_idle_session != undefined ? this.data.min_idle_session : 0 },
|
||||
set(v:number) { this.data.min_idle_session = v>0 ? v : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-card subtitle="Direct">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.direct.overrideAddr')"
|
||||
hide-details
|
||||
v-model="data.override_address">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.direct.overridePort')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="override_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
override_port: {
|
||||
get() { return this.$props.data.override_port ? this.$props.data.override_port : '' },
|
||||
set(newValue: any) { this.$props.data.override_port = newValue.length == 0 || newValue == 0 ? undefined : parseInt(newValue) }
|
||||
},
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<v-card subtitle="HTTP">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.un')"
|
||||
hide-details
|
||||
v-model="username">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
v-model="password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="data.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="data" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Headers from '@/components/Headers.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
username: {
|
||||
get(): string { return this.data.username?.length > 0 ? this.data.username : '' },
|
||||
set(v:string) { this.data.username = v.length > 0 ? v : undefined },
|
||||
},
|
||||
password: {
|
||||
get(): string { return this.data.password?.length > 0 ? this.data.password : '' },
|
||||
set(v:string) { this.data.password = v.length > 0 ? v : undefined },
|
||||
},
|
||||
},
|
||||
components: { Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<v-card subtitle="Hysteria">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('stats.upload')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('stats.Mbps')"
|
||||
v-model.number="up_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('stats.download')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('stats.Mbps')"
|
||||
min="0"
|
||||
v-model.number="down_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.hy.obfs')"
|
||||
hide-details
|
||||
v-model="data.obfs">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction=='out'">
|
||||
<v-text-field
|
||||
:label="$t('types.hy.auth')"
|
||||
hide-details
|
||||
v-model="data.auth_str">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction=='out'">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.disable_mtu_discovery" color="primary" label="Disable MTU discovery" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.recv_window_conn != undefined">
|
||||
<v-text-field
|
||||
label="Recv window conn"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="data.recv_window_conn">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.recv_window != undefined">
|
||||
<v-text-field
|
||||
label="Recv window"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="data.recv_window">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.recv_window_client != undefined">
|
||||
<v-text-field
|
||||
label="Recv window client"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="data.recv_window_client">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.max_conn_client != undefined">
|
||||
<v-text-field
|
||||
label="Max conn client"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="data.max_conn_client">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hyOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRsvConn" color="primary" label="Recv window conn" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="direction=='out'">
|
||||
<v-switch v-model="optionRsvWin" color="primary" label="Recv window" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="direction=='in'">
|
||||
<v-switch v-model="optionRsvClnt" color="primary" label="Recv window client" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="direction=='in'">
|
||||
<v-switch v-model="optionMaxConn" color="primary" label="Max conn client" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['direction','data'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionRsvConn: {
|
||||
get(): boolean { return this.$props.data.recv_window_conn != undefined },
|
||||
set(v:boolean) { this.$props.data.recv_window_conn = v ? 15728640 : undefined }
|
||||
},
|
||||
optionRsvWin: {
|
||||
get(): boolean { return this.$props.data.recv_window != undefined },
|
||||
set(v:boolean) { this.$props.data.recv_window = v ? 67108864 : undefined }
|
||||
},
|
||||
optionRsvClnt: {
|
||||
get(): boolean { return this.$props.data.recv_window_client != undefined },
|
||||
set(v:boolean) { this.$props.data.recv_window_client = v ? 67108864 : undefined }
|
||||
},
|
||||
optionMaxConn: {
|
||||
get(): boolean { return this.$props.data.max_conn_client != undefined },
|
||||
set(v:boolean) { this.$props.data.max_conn_client = v ? 1024 : undefined }
|
||||
},
|
||||
down_mbps: {
|
||||
get() { return this.$props.data.down_mbps ? this.$props.data.down_mbps : 0 },
|
||||
set(newValue:any) {
|
||||
if (newValue.length != 0 ){
|
||||
this.$props.data.down_mbps = newValue
|
||||
this.$props.data.down = "" + newValue + " Mbps"
|
||||
} else {
|
||||
this.$props.data.down_mbps = 0
|
||||
this.$props.data.down = "0 Mbps"
|
||||
}
|
||||
}
|
||||
},
|
||||
up_mbps: {
|
||||
get() { return this.$props.data.up_mbps ? this.$props.data.up_mbps : 0 },
|
||||
set(newValue:number) { this.$props.data.up_mbps = newValue > 0 ? newValue : 0 }
|
||||
},
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<v-card subtitle="Hysteria2">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction == 'in'">
|
||||
<v-switch v-model="data.ignore_client_bandwidth" color="primary" :label="$t('types.hy.ignoreBw')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="!data.ignore_client_bandwidth">
|
||||
<v-text-field
|
||||
:label="$t('stats.upload')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('stats.Mbps')"
|
||||
min="0"
|
||||
v-model.number="up_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="!data.ignore_client_bandwidth">
|
||||
<v-text-field
|
||||
:label="$t('stats.download')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('stats.Mbps')"
|
||||
min="0"
|
||||
v-model.number="down_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.obfs != undefined">
|
||||
<v-text-field
|
||||
:label="$t('types.hy.obfs')"
|
||||
hide-details
|
||||
v-model="data.obfs.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="direction == 'in'">
|
||||
<v-card subtitle="Hysteria2 Masquerade" v-if="data.masquerade != undefined">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select v-model="masqueradeType" hide-details :label="$t('type')" :items="masqTypes"></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8" v-if="masqueradeType == ''">
|
||||
<v-text-field
|
||||
label="HTTP3 server on auth fails"
|
||||
placeholder="file:///var/www | http://127.0.0.1:8080"
|
||||
v-model="data.masquerade"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8" v-if="masqueradeType == 'file'">
|
||||
<v-text-field
|
||||
label="File server root directory"
|
||||
placeholder="/var/www"
|
||||
v-model="data.masquerade.directory"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="masqueradeType == 'string'">
|
||||
<v-text-field
|
||||
label="HTTP Code"
|
||||
type="number"
|
||||
min="100"
|
||||
max="599"
|
||||
v-model.number="data.masquerade.status_code"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="masqueradeType == 'proxy'">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
label="Target URL"
|
||||
placeholder="http://example.com:8080"
|
||||
v-model="data.masquerade.url"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch
|
||||
label="Rewrite Host"
|
||||
v-model="data.masquerade.rewrite_host"
|
||||
color="primary"
|
||||
hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="masqueradeType == 'string'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
label="Content"
|
||||
v-model="data.masquerade.content"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="data.masquerade" />
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
v-model="data.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8" v-if="optionMPort">
|
||||
<v-text-field
|
||||
:label="$t('rule.portRange') + ' ' + $t('commaSeparated')"
|
||||
v-model="server_ports">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionMPort">
|
||||
<v-text-field
|
||||
:label="$t('ruleset.interval')"
|
||||
type="number"
|
||||
min="0"
|
||||
:suffix="$t('date.s')"
|
||||
v-model.number="hop_interval">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hy2Options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionObfs" color="primary" :label="$t('types.hy.obfs')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<template v-if="direction == 'in'">
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMasq" color="primary" label="Masquerade" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMPort" color="primary" :label="$t('rule.portRange')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
import Headers from '@/components/Headers.vue'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: ['direction', 'data'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
masqTypes: [
|
||||
{ title: i18n.global.t('rule.simple'), value: '' },
|
||||
{ title: "File server", value: "file" },
|
||||
{ title: "Reverse Proxy", value: "proxy" },
|
||||
{ title: "Fixed response", value: "string" },
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
down_mbps: {
|
||||
get() { return this.$props.data.down_mbps?? 0 },
|
||||
set(v:number) { this.$props.data.down_mbps = v>0 ? v : undefined }
|
||||
},
|
||||
up_mbps: {
|
||||
get() { return this.$props.data.up_mbps?? 0 },
|
||||
set(v:number) { this.$props.data.up_mbps = v>0 ? v : undefined }
|
||||
},
|
||||
server_ports: {
|
||||
get() { return this.$props.data.server_ports?.join(',')?? [] },
|
||||
set(v:string) { this.$props.data.server_ports = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
masqueradeType: {
|
||||
get() { return typeof this.$props.data.masquerade === 'object' ? this.$props.data.masquerade.type?? '' : '' },
|
||||
set(v:string) {
|
||||
if (v == '') {
|
||||
this.$props.data.masquerade = ''
|
||||
} else {
|
||||
this.$props.data.masquerade = { type: v }
|
||||
}
|
||||
}
|
||||
},
|
||||
hop_interval: {
|
||||
get() { return this.$props.data.hop_interval? parseInt(this.$props.data.hop_interval.replace('s','')) : 0 },
|
||||
set(v:number) { this.$props.data.hop_interval = v>0 ? v + 's' : undefined }
|
||||
},
|
||||
optionObfs: {
|
||||
get(): boolean { return this.$props.data.obfs != undefined },
|
||||
set(v:boolean) { this.$props.data.obfs = v ? { type: "salamander", password: "" } : undefined }
|
||||
},
|
||||
optionMasq: {
|
||||
get(): boolean { return this.$props.data.masquerade != undefined },
|
||||
set(v:boolean) { this.$props.data.masquerade = v ? "" : undefined }
|
||||
},
|
||||
optionMPort: {
|
||||
get(): boolean { return this.$props.data.server_ports != undefined },
|
||||
set(v:boolean) { this.$props.data.server_ports = v ? [] : undefined }
|
||||
}
|
||||
},
|
||||
components: { Network, Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-subtitle v-if="direction != 'out_json'">Naive</v-card-subtitle>
|
||||
<!-- Inbound -->
|
||||
<template v-if="direction === 'in'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.naive.quicCongestion')"
|
||||
:items="inbCngs"
|
||||
v-model="data.quic_congestion_control"
|
||||
@click:clear="delete data.quic_congestion_control"
|
||||
clearable>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<!-- Outbound -->
|
||||
<template v-if="['out', 'out_json'].includes(direction)">
|
||||
<v-row v-if="direction === 'out'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.un')"
|
||||
hide-details
|
||||
v-model="data.username">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
type="password"
|
||||
v-model="data.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.naive.insecureConcurrency')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="insecure_concurrency">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="udpOverTcp" color="primary" :label="$t('types.naive.udpOverTcp')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="direction === 'out'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.quic" color="primary" :label="$t('types.naive.quic')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.quic">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.naive.quicCongestion')"
|
||||
:items="outCngs"
|
||||
@click:clear="delete data.quic_congestion_control"
|
||||
clearable
|
||||
v-model="data.quic_congestion_control">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="extra_headers" />
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
import Headers from '@/components/Headers.vue'
|
||||
|
||||
export default {
|
||||
props: ['data', 'direction'],
|
||||
data() {
|
||||
return {
|
||||
inbCngs: [
|
||||
{ title: 'BBR', value: 'bbr'},
|
||||
{ title: 'BBR Standard', value: 'bbr_standard'},
|
||||
{ title: 'BBRv2', value: 'bbr2'},
|
||||
{ title: 'BBRv2 variant', value: 'bbr2_variant'},
|
||||
{ title: 'Cubic', value: 'cubic'},
|
||||
{ title: 'New Reno', value: 'reno'},
|
||||
],
|
||||
outCngs: [
|
||||
{ title: 'BBR', value: 'bbr'},
|
||||
{ title: 'BBR2', value: 'bbr2'},
|
||||
{ title: 'Cubic', value: 'cubic'},
|
||||
{ title: 'Reno', value: 'reno'},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
udpOverTcp: {
|
||||
get(): boolean {
|
||||
const d = this.$props.data
|
||||
return d?.udp_over_tcp === true || (d?.udp_over_tcp && (d.udp_over_tcp as any)?.enabled)
|
||||
},
|
||||
set(v: boolean) {
|
||||
if (v) {
|
||||
this.$props.data.udp_over_tcp = { enabled: true }
|
||||
} else {
|
||||
this.$props.data.udp_over_tcp = false
|
||||
}
|
||||
}
|
||||
},
|
||||
insecure_concurrency: {
|
||||
get(): number { return this.$props.data?.insecure_concurrency ?? 0 },
|
||||
set(v: number) {
|
||||
this.$props.data.insecure_concurrency = v > 0 ? v : undefined
|
||||
}
|
||||
},
|
||||
extra_headers(): any {
|
||||
const d = this.$props.data
|
||||
return new Proxy({}, {
|
||||
get(_, prop) {
|
||||
if (prop === 'headers') return d?.extra_headers ?? {}
|
||||
return undefined
|
||||
},
|
||||
set(_, prop, value) {
|
||||
if (prop === 'headers') {
|
||||
d.extra_headers = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
components: { Network, Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-card subtitle="ShadowTls">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="[1,2,3]"
|
||||
:label="$t('version')"
|
||||
v-model="version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.version > 1">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
v-model="data.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
version: {
|
||||
get() { return this.$props.data.version ?? 3 },
|
||||
set(v: number) {
|
||||
this.$props.data.version = v
|
||||
if (v==1) {
|
||||
delete this.$props.data.password
|
||||
} else if (this.$props.data.password === undefined ) {
|
||||
this.$props.data.password = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<v-card subtitle="Selector">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-combobox
|
||||
v-model="data.outbounds"
|
||||
:items="tags"
|
||||
:label="$t('pages.outbounds')"
|
||||
multiple
|
||||
@update:model-value="updateDefault"
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-combobox
|
||||
v-model="data.default"
|
||||
:items="data.outbounds"
|
||||
:label="$t('types.lb.defaultOut')"
|
||||
clearable
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-switch v-model="data.interrupt_exist_connections" color="primary" :label="$t('types.lb.interruptConn')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['data','tags'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
updateDefault() {
|
||||
if (!this.$props.data.outbounds?.includes(this.$props.data.default)) {
|
||||
delete this.$props.data.default
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<v-card subtitle="ShadowTls">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="[1,2,3]"
|
||||
:label="$t('version')"
|
||||
:disabled="data.id > 0"
|
||||
v-model="version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.password != undefined">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
v-model="data.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="Inbound.wildcard_sni != undefined">
|
||||
<v-select label="Wildcard SNI" :items="['off', 'authed', 'all']" clearable v-model="Inbound.wildcard_sni"></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.shdwTls.hs')"
|
||||
hide-details
|
||||
v-model="Inbound.handshake.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Dial :dial="Inbound.handshake" />
|
||||
<v-row v-if="Inbound.handshake_for_server_name != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.shdwTls.addHS')"
|
||||
hide-details
|
||||
v-model="handshake_server">
|
||||
<template v-slot:append>
|
||||
<v-chip
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="elevated"
|
||||
:disabled="handshake_server == ''"
|
||||
@click="addHandshakeServer()">
|
||||
<v-icon icon="mdi-plus" />
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card
|
||||
v-for="(value, key) in Inbound.handshake_for_server_name"
|
||||
border
|
||||
density="compact"
|
||||
style="margin: 5px;"
|
||||
color="background">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ key }}
|
||||
<v-icon icon="mdi-delete" color="error" size="small"
|
||||
@click="Inbound.handshake_for_server_name ? delete Inbound.handshake_for_server_name[key] : null" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.shdwTls.hs')"
|
||||
hide-details
|
||||
v-model="value.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="value.server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Dial :dial="value" />
|
||||
</v-card>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ShadowTLS } from '@/types/inbounds'
|
||||
import Dial from '../Dial.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
handshake_server: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addHandshakeServer() {
|
||||
this.data.handshake_for_server_name[this.handshake_server] = {}
|
||||
// Clear the input field after adding the server
|
||||
this.handshake_server = ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.version = this.Inbound.version
|
||||
},
|
||||
computed: {
|
||||
version: {
|
||||
get() {
|
||||
this.version = this.Inbound.version
|
||||
return this.Inbound.version
|
||||
},
|
||||
set(newValue: any) {
|
||||
switch (newValue) {
|
||||
case 1:
|
||||
delete this.Inbound.password
|
||||
delete this.Inbound.handshake_for_server_name
|
||||
delete this.Inbound.wildcard_sni
|
||||
break
|
||||
case 2:
|
||||
if (!this.Inbound.password) {
|
||||
this.Inbound.password = ""
|
||||
}
|
||||
if (!this.Inbound.handshake_for_server_name) {
|
||||
this.Inbound.handshake_for_server_name = {}
|
||||
}
|
||||
delete this.Inbound.wildcard_sni
|
||||
break
|
||||
case 3:
|
||||
delete this.Inbound.password
|
||||
if (!this.Inbound.handshake_for_server_name) {
|
||||
this.Inbound.handshake_for_server_name = {}
|
||||
}
|
||||
if (!this.Inbound.wildcard_sni) {
|
||||
this.Inbound.wildcard_sni = ""
|
||||
}
|
||||
break
|
||||
}
|
||||
this.Inbound.version = newValue
|
||||
}
|
||||
},
|
||||
Inbound(): ShadowTLS {
|
||||
return <ShadowTLS>this.$props.data
|
||||
},
|
||||
server_port: {
|
||||
get() { return this.Inbound.handshake.server_port ? this.Inbound.handshake.server_port : 443 },
|
||||
set(newValue: any) { this.Inbound.handshake.server_port = newValue.length == 0 || newValue == 0 ? 443 : parseInt(newValue) }
|
||||
},
|
||||
},
|
||||
components: { Dial }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<v-card subtitle="Shadowsocks">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('in.ssMethod')"
|
||||
:items="ssMethods"
|
||||
@update:model-value="direction == 'in' ? changeMethod($event) : undefined"
|
||||
v-model="data.method">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction == 'out'">
|
||||
<UoT :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction == 'in'">
|
||||
<v-switch
|
||||
v-model="data.managed"
|
||||
color="primary"
|
||||
:label="$t('in.ssManageable')"
|
||||
hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="data.method != 'none' || direction == 'out'">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
v-model="data.password"
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
:append-inner-icon="direction == 'in' ? 'mdi-refresh' : undefined"
|
||||
@click:append-inner="changeMethod(data.method)">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
import UoT from '@/components/UoT.vue'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
|
||||
export default {
|
||||
props: ['direction','data'],
|
||||
data() {
|
||||
return {
|
||||
ssMethods: [
|
||||
"none",
|
||||
"aes-128-gcm",
|
||||
"aes-192-gcm",
|
||||
"aes-256-gcm",
|
||||
"chacha20-ietf-poly1305",
|
||||
"xchacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm",
|
||||
"2022-blake3-aes-256-gcm",
|
||||
"2022-blake3-chacha20-poly1305"
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeMethod(ssMethod :string) {
|
||||
if (ssMethod.startsWith('2022')) {
|
||||
this.$props.data.password = ssMethod == "2022-blake3-aes-128-gcm" ? RandomUtil.randomShadowsocksPassword(16) : RandomUtil.randomShadowsocksPassword(32)
|
||||
} else if (ssMethod == 'none') {
|
||||
delete this.$props.data.password
|
||||
} else {
|
||||
this.$props.data.password = RandomUtil.randomSeq(10)
|
||||
}
|
||||
}
|
||||
},
|
||||
components: { Network, UoT }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<v-card subtitle="SOCKS">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.un')"
|
||||
hide-details
|
||||
v-model="username">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.pw')"
|
||||
hide-details
|
||||
v-model="password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="['4','4a','5']"
|
||||
:label="$t('version')"
|
||||
v-model="data.version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<UoT :data="data" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
import UoT from '@/components/UoT.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
username: {
|
||||
get(): string { return this.data.username?.length > 0 ? this.data.username : '' },
|
||||
set(v:string) { this.data.username = v.length > 0 ? v : undefined },
|
||||
},
|
||||
password: {
|
||||
get(): string { return this.data.password?.length > 0 ? this.data.password : '' },
|
||||
set(v:string) { this.data.password = v.length > 0 ? v : undefined },
|
||||
},
|
||||
},
|
||||
components: { Network, UoT }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<v-card subtitle="SSH">
|
||||
<template v-if="optionKey">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="usePath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="data.private_key=undefined; data.private_key_path=''"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="data.private_key_path=undefined; data.private_key=''"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="usePath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
v-model="data.private_key_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
v-model="data.private_key">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('types.ssh.passphrase')"
|
||||
hide-details
|
||||
v-model="data.private_key_passphrase">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.user" :label="$t('types.un')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.password" :label="$t('types.pw')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-row v-if="optionHostKey">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('types.ssh.hostKey')"
|
||||
hide-details
|
||||
v-model="host_key">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.host_key_algorithms != undefined">
|
||||
<v-text-field v-model="algorithms" :label="$t('types.ssh.algorithm') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.client_version != undefined">
|
||||
<v-text-field v-model="data.client_version" :label="$t('types.ssh.clientVer')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.ssh.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionKey" color="primary" label="SSH Key" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionHostKey" color="primary" :label="$t('types.ssh.hostKey')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionAlgorithms" color="primary" :label="$t('types.ssh.algorithm')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionVer" color="primary" :label="$t('types.ssh.clientVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
usePath: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionKey: {
|
||||
get(): boolean { return this.data.private_key != undefined || this.data.private_key_path != undefined },
|
||||
set(v:boolean) {
|
||||
this.usePath = 0
|
||||
if (v) {
|
||||
this.$props.data.private_key_path = ""
|
||||
delete this.$props.data.user
|
||||
delete this.$props.data.password
|
||||
} else {
|
||||
delete this.$props.data.private_key_path
|
||||
delete this.$props.data.private_key
|
||||
delete this.$props.data.private_key_passphrase
|
||||
}
|
||||
}
|
||||
},
|
||||
optionHostKey: {
|
||||
get(): boolean { return this.data.host_key != undefined },
|
||||
set(v:boolean) { this.data.host_key = v ? '' : undefined }
|
||||
},
|
||||
optionAlgorithms: {
|
||||
get(): boolean { return this.data.host_key_algorithms != undefined },
|
||||
set(v:boolean) { this.data.host_key_algorithms = v ? [] : undefined }
|
||||
},
|
||||
optionVer: {
|
||||
get(): boolean { return this.data.client_version != undefined },
|
||||
set(v:boolean) { this.data.client_version = v ? 'SSH-2.0-OpenSSH_7.4p1' : undefined }
|
||||
},
|
||||
host_key: {
|
||||
get(): string { return this.$props.data.host_key ? this.$props.data.host_key.join('\n') : '' },
|
||||
set(v:string) { this.$props.data.host_key = v.split('\n') }
|
||||
},
|
||||
algorithms: {
|
||||
get() { return this.$props.data.host_key_algorithms ? this.$props.data.host_key_algorithms.join(',') : '' },
|
||||
set(v:string) { this.$props.data.host_key_algorithms = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-card subtitle="TProxy">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="inbound" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<v-card subtitle="Talescale">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="data.ephemeral" :label="$t('types.ts.ephemeral')"></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="data.accept_routes" :label="$t('types.ts.acceptRoutes')"></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionStateDir">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="data.state_directory" :label="$t('types.ts.stateDir')"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionAuth">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="data.auth_key" :label="$t('types.ts.authKey')"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionCtrlUrl">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="data.control_url" :label="$t('types.ts.controlUrl')"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionHostname">
|
||||
<v-text-field v-model="data.hostname" :label="$t('types.ts.hostname')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionUdpTimeout">
|
||||
<v-text-field type="number" v-model.number="udpTimeout" min="1" :suffix="$t('date.s')" :label="$t('types.ts.udpTimeout')"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionExitNode">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.exit_node" :label="$t('types.ts.exitNode')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="data.exit_node_allow_lan_access" :label="$t('types.ts.allowLanAccess')"></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionRelay">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="data.relay_server_port" type="number" min="0" :label="$t('types.ts.relayServerPort')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="relay_endpoints" :label="$t('types.ts.relayEndpoints') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionSysIf">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="data.system_interface" :label="$t('types.ts.systemInterface')"></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.system_interface">
|
||||
<v-text-field v-model="data.system_interface_name" :label="$t('types.ts.sysIfName')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.system_interface">
|
||||
<v-text-field v-model.number="data.system_interface_mtu" type="number" min="0" :label="$t('types.ts.sysIfMtu')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionAdvRoutes">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="advertise_routes" :label="$t('types.ts.advRoutes') + ' ' + $t('commaSeparated')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="data.advertise_exit_node" :label="$t('types.ts.advExitNode')"></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.ts.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionStateDir" color="primary" :label="$t('types.ts.stateDir')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionAuth" color="primary" :label="$t('types.ts.authKey')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCtrlUrl" color="primary" :label="$t('types.ts.controlUrl')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionHostname" color="primary" :label="$t('types.ts.hostname')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionExitNode" color="primary" :label="$t('types.ts.exitNode')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRelay" color="primary" :label="$t('types.ts.relayServer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSysIf" color="primary" :label="$t('types.ts.systemInterface')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionAdvRoutes" color="primary" :label="$t('types.ts.advRoutes')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUdpTimeout" color="primary" :label="$t('types.ts.udpTimeout')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionStateDir: {
|
||||
get() { return this.$props.data?.state_directory !== undefined },
|
||||
set(v: boolean) { this.$props.data.state_directory = v ? "$HOME/.tailscale" : undefined }
|
||||
},
|
||||
optionAuth: {
|
||||
get() { return this.$props.data?.auth_key !== undefined },
|
||||
set(v: boolean) { this.$props.data.auth_key = v ? "" : undefined }
|
||||
},
|
||||
optionCtrlUrl: {
|
||||
get() { return this.$props.data?.control_url !== undefined },
|
||||
set(v: boolean) { this.$props.data.control_url = v ? "https://controlplane.tailscale.com" : undefined }
|
||||
},
|
||||
optionHostname: {
|
||||
get() { return this.$props.data?.hostname !== undefined },
|
||||
set(v: boolean) { this.$props.data.hostname = v ? "localhost" : undefined }
|
||||
},
|
||||
optionExitNode: {
|
||||
get() { return this.$props.data?.exit_node !== undefined },
|
||||
set(v: boolean) {
|
||||
if (v) {
|
||||
this.$props.data.exit_node = ""
|
||||
} else {
|
||||
delete this.$props.data.exit_node
|
||||
delete this.$props.data.exit_node_allow_lan_access
|
||||
}
|
||||
}
|
||||
},
|
||||
optionAdvRoutes: {
|
||||
get() { return this.$props.data?.advertise_routes !== undefined },
|
||||
set(v: boolean) {
|
||||
if (v) {
|
||||
this.$props.data.advertise_routes = []
|
||||
} else {
|
||||
delete this.$props.data.advertise_routes
|
||||
delete this.$props.data.advertise_exit_node
|
||||
}
|
||||
}
|
||||
},
|
||||
optionRelay: {
|
||||
get() { return this.$props.data?.relay_server_port !== undefined || (this.$props.data?.relay_server_static_endpoints?.length ?? 0) > 0 },
|
||||
set(v: boolean) {
|
||||
if (v) {
|
||||
this.$props.data.relay_server_port = 0
|
||||
this.$props.data.relay_server_static_endpoints = []
|
||||
} else {
|
||||
delete this.$props.data.relay_server_port
|
||||
delete this.$props.data.relay_server_static_endpoints
|
||||
}
|
||||
}
|
||||
},
|
||||
optionSysIf: {
|
||||
get() { return this.$props.data?.system_interface !== undefined },
|
||||
set(v: boolean) {
|
||||
if (v) {
|
||||
this.$props.data.system_interface = false
|
||||
} else {
|
||||
delete this.$props.data.system_interface
|
||||
delete this.$props.data.system_interface_name
|
||||
delete this.$props.data.system_interface_mtu
|
||||
}
|
||||
}
|
||||
},
|
||||
optionUdpTimeout: {
|
||||
get() { return this.$props.data?.udp_timeout !== undefined },
|
||||
set(v: boolean) { this.$props.data.udp_timeout = v ? '30s' : undefined }
|
||||
},
|
||||
udpTimeout: {
|
||||
get() { return this.$props.data?.udp_timeout? this.$props.data.udp_timeout.replace('s','') : '' },
|
||||
set(v: number) { this.$props.data.udp_timeout = v>1 ? v + 's' : '30s' }
|
||||
},
|
||||
advertise_routes: {
|
||||
get() { return this.$props.data?.advertise_routes?.join(',') ?? "" },
|
||||
set(v: string) { this.$props.data.advertise_routes = v.length > 0 ? v.split(',') : [] }
|
||||
},
|
||||
relay_endpoints: {
|
||||
get() { return this.$props.data?.relay_server_static_endpoints?.join(',') ?? "" },
|
||||
set(v: string) { this.$props.data.relay_server_static_endpoints = v.length > 0 ? v.split(',') : [] }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<v-card subtitle="Tor">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.executable_path" :label="$t('types.tor.execPath')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.data_directory" :label="$t('types.tor.dataDir')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="extra_args" :label="$t('types.tor.extArgs') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="v-card-subtitle" style="margin: 10px;">Torrc
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="add_torrc_option"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</div>
|
||||
<v-row v-for="(torrc, index) in torrc_options">
|
||||
<v-col cols="auto" align-self="center" justify-self="center">
|
||||
<v-icon @click="del_torrc_option(index)" color="error" icon="mdi-delete" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('objects.key')"
|
||||
hide-details
|
||||
@input="update_key(index,$event.target.value)"
|
||||
v-model="torrc.name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('objects.value')"
|
||||
hide-details
|
||||
@input="update_value(index,$event.target.value)"
|
||||
v-model="torrc.value">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
type torrc_option = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
add_torrc_option() {
|
||||
this.torrc_options = [...this.torrc_options, {name: "", value: ""}]
|
||||
},
|
||||
del_torrc_option(i:number) {
|
||||
let h = this.torrc_options
|
||||
h.splice(i,1)
|
||||
this.torrc_options = h
|
||||
},
|
||||
update_key(i:number,k:string) {
|
||||
let h = this.torrc_options
|
||||
h[i].name = k
|
||||
this.torrc_options = h
|
||||
},
|
||||
update_value(i:number,v:string) {
|
||||
let h = this.torrc_options
|
||||
h[i].value = v
|
||||
this.torrc_options = h
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
torrc_options: {
|
||||
get() :torrc_option[] {
|
||||
let options: torrc_option[] = []
|
||||
const h = this.$props.data.torrc
|
||||
if (h) {
|
||||
Object.keys(h).forEach(key => {
|
||||
if (Array.isArray(h[key])){
|
||||
h[key].forEach((v:string) => options.push({ name: key, value: v }))
|
||||
} else {
|
||||
options.push({ name: key, value: h[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
return options
|
||||
},
|
||||
set(v:torrc_option[]) {
|
||||
if (v.length>0) {
|
||||
let torrc:any = {}
|
||||
v.forEach((h:torrc_option) => {
|
||||
if (torrc[h.name]) {
|
||||
if (Array.isArray(torrc[h.name])) {
|
||||
torrc[h.name].push(h.value)
|
||||
} else {
|
||||
torrc[h.name] = [torrc[h.name], h.value]
|
||||
}
|
||||
} else {
|
||||
torrc[h.name] = h.value
|
||||
}
|
||||
})
|
||||
this.$props.data.torrc = torrc
|
||||
} else {
|
||||
this.$props.data.torrc = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
extra_args: {
|
||||
get() { return this.$props.data.extra_args?.join(',') },
|
||||
set(v:string) { this.$props.data.extra_args = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<v-card subtitle="Trojan">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.password" :label="$t('types.pw')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<v-card subtitle="TUIC">
|
||||
<v-row v-if="direction == 'out'">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="data.uuid" label="UUID" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.password" :label="$t('types.pw')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="UDP Relay Mode"
|
||||
:items="['native', 'quic']"
|
||||
clearable
|
||||
@click:clear="delete data.udp_relay_mode"
|
||||
v-model="data.udp_relay_mode">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="UDP Over Stream" v-model="data.udp_over_stream" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.tuic.congControl')"
|
||||
:items="congestion_controls"
|
||||
v-model="data.congestion_control">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="Zero-RTT Handshake" v-model="data.zero_rtt_handshake" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction == 'in'">
|
||||
<v-text-field
|
||||
:label="$t('types.tuic.authTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('date.s')"
|
||||
min="1"
|
||||
v-model.number="auth_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.tuic.hb')"
|
||||
hide-details
|
||||
type="number"
|
||||
:suffix="$t('date.s')"
|
||||
min="1"
|
||||
v-model.number="heartbeat">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['direction', 'data'],
|
||||
data() {
|
||||
return {
|
||||
congestion_controls: [
|
||||
"cubic","new_reno", "bbr"
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
auth_timeout: {
|
||||
get() { return this.$props.data.auth_timeout ? parseInt(this.$props.data.auth_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.data.auth_timeout = newValue ? newValue + 's' : '' }
|
||||
},
|
||||
heartbeat: {
|
||||
get() { return this.$props.data.heartbeat ? parseInt(this.$props.data.heartbeat.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.data.heartbeat = newValue ? newValue + 's' : '' }
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<v-card subtitle="Tun">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="addrs" :label="$t('types.tun.addr') + ' ' + $t('commaSeparated')" placeholder="172.18.0.1/30" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="data.interface_name" :label="$t('types.tun.ifName')" placeholder="tun0" hide-details clearable @click:clear="delete data.interface_name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field type="number" v-model.number="data.mtu" label="MTU" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="udpTimeout"
|
||||
label="UDP timeout"
|
||||
min="1"
|
||||
:suffix="$t('date.m')"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="data.stack"
|
||||
label="Stack"
|
||||
:items="['system','gvisor','mixed']"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.endpoint_independent_nat" color="primary" label="Independent NAT" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="autoRoute" color="primary" label="Auto Route" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="autoRoute">
|
||||
<v-switch v-model="data.auto_redirect" color="primary" label="Auto Redirect" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="autoRoute">
|
||||
<v-switch v-model="data.strict_route" color="primary" label="Strict Route" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="autoRoute && data.auto_redirect">
|
||||
<v-switch v-model="data.exclude_mptcp" color="primary" :label="$t('types.tun.excludeMptcp')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="autoRoute && data.auto_redirect">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="fallbackRuleIndex"
|
||||
:label="$t('types.tun.fallbackRuleIndex')"
|
||||
min="0"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
addrs: {
|
||||
get() { return this.$props.data.address?.join(',') },
|
||||
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
udpTimeout: {
|
||||
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
|
||||
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
|
||||
},
|
||||
autoRoute: {
|
||||
get() { return this.$props.data.auto_route ?? false },
|
||||
set(v:boolean) {
|
||||
this.$props.data.auto_route = v
|
||||
this.$props.data.auto_redirect = v ? false : undefined
|
||||
this.$props.data.strict_route = v ? false : undefined
|
||||
}
|
||||
},
|
||||
fallbackRuleIndex: {
|
||||
get() { return this.$props.data.auto_redirect_iproute2_fallback_rule_index ?? 32768 },
|
||||
set(v: number) {
|
||||
const val = typeof v === 'number' && !isNaN(v) && v >= 0 ? v : undefined
|
||||
this.$props.data.auto_redirect_iproute2_fallback_rule_index = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<v-card subtitle="URL Test">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-combobox
|
||||
v-model="data.outbounds"
|
||||
:items="tags"
|
||||
:label="$t('pages.outbounds')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" v-if="optionUrl">
|
||||
<v-text-field v-model="data.url" :label="$t('types.lb.testUrl')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionInterval">
|
||||
<v-text-field
|
||||
:label="$t('types.lb.interval')"
|
||||
hide-details
|
||||
type="number"
|
||||
min="3"
|
||||
:suffix="$t('date.s')"
|
||||
v-model.number="interval"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionTolerance">
|
||||
<v-text-field
|
||||
:label="$t('types.lb.tolerance')"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
:suffix="$t('date.ms')"
|
||||
v-model.number="tolerance"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIdle">
|
||||
<v-text-field
|
||||
:label="$t('transport.idleTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
:suffix="$t('date.m')"
|
||||
v-model.number="idle_timeout"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-switch v-model="data.interrupt_exist_connections" color="primary" :label="$t('types.lb.interruptConn')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.lb.urlTestOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUrl" color="primary" :label="$t('types.lb.testUrl')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionInterval" color="primary" :label="$t('types.lb.interval')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTolerance" color="primary" :label="$t('types.lb.tolerance')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionIdle" color="primary" :label="$t('transport.idleTimeout')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['data', 'tags'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionUrl: {
|
||||
get(): boolean { return this.$props.data.url != undefined },
|
||||
set(v:boolean) { this.$props.data.url = v ? 'https://www.gstatic.com/generate_204' : undefined }
|
||||
},
|
||||
optionInterval: {
|
||||
get(): boolean { return this.$props.data.interval != undefined },
|
||||
set(v:boolean) { this.$props.data.interval = v ? '3s' : undefined }
|
||||
},
|
||||
optionTolerance: {
|
||||
get(): boolean { return this.$props.data.tolerance != undefined },
|
||||
set(v:boolean) { this.$props.data.tolerance = v ? 50 : undefined }
|
||||
},
|
||||
optionIdle: {
|
||||
get(): boolean { return this.$props.data.idle_timeout != undefined },
|
||||
set(v:boolean) { this.$props.data.idle_timeout = v ? '30m' : undefined }
|
||||
},
|
||||
interval: {
|
||||
get() { return this.$props.data.interval ? parseInt(this.$props.data.interval.replace('s','')) : 3 },
|
||||
set(v:number) { this.$props.data.interval = v > 0 ? v + 's' : '3s' }
|
||||
},
|
||||
tolerance: {
|
||||
get() { return this.$props.data.tolerance ? parseInt(this.$props.data.tolerance) : 0 },
|
||||
set(v:number) { this.$props.data.tolerance = v > 0 ? v : 0 }
|
||||
},
|
||||
idle_timeout: {
|
||||
get() { return this.$props.data.idle_timeout ? parseInt(this.$props.data.idle_timeout.replace('m','')) : 30 },
|
||||
set(v:number) { this.$props.data.idle_timeout = v > 0 ? v + 'm' : '0m' }
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<v-card subtitle="VLESS">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="data.uuid" label="UUID" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vless.flow')"
|
||||
:items="['','xtls-rprx-vision']"
|
||||
v-model="data.flow">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vless.udpEnc')"
|
||||
:items="['none','packetaddr','xudp']"
|
||||
v-model="packet_encoding">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
packet_encoding: {
|
||||
get() { return this.$props.data.packet_encoding != undefined ? this.$props.data.packet_encoding : 'none' },
|
||||
set(newValue:string) { this.$props.data.packet_encoding = newValue != "none" ? newValue : undefined }
|
||||
},
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<v-card subtitle="VMESS">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="data.uuid" label="UUID" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Alter ID"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
v-model.number="data.alter_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vmess.security')"
|
||||
:items="securities"
|
||||
v-model="data.security">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vless.udpEnc')"
|
||||
:items="['none','packetaddr','xudp']"
|
||||
v-model="packet_encoding">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :data="data" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
securities: [
|
||||
"auto",
|
||||
"none",
|
||||
"zero",
|
||||
"aes-128-gcm",
|
||||
"aes-128-ctr",
|
||||
"chacha20-poly1305",
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
packet_encoding: {
|
||||
get() { return this.$props.data.packet_encoding != undefined ? this.$props.data.packet_encoding : 'none' },
|
||||
set(newValue:string) { this.$props.data.packet_encoding = newValue != "none" ? newValue : undefined }
|
||||
},
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<v-card subtitle="Warp">
|
||||
<template v-if="data.id>0">
|
||||
<table dir="ltr" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Device ID</td>
|
||||
<td>{{ data.ext.device_id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Access Token</td>
|
||||
<td>{{ data.ext.access_token }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('types.wg.privKey') }}</td>
|
||||
<td>{{ data.private_key }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('types.wg.localIp') }}</td>
|
||||
<td>{{ data.address.join(',') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<v-text-field
|
||||
v-model="data.ext.license_key"
|
||||
label="License Key"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<v-card :subtitle="$t('types.wg.peer')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
v-model="data.peers[0].address">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
v-model.number="data.peers[0].port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<table dir="ltr" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('types.wg.pubKey') }}</td>
|
||||
<td>{{ data.peers[0].public_key }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('types.wg.allowedIp') }}</td>
|
||||
<td>{{ data.peers[0].allowed_ips.join(',') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reserved</td>
|
||||
<td>[{{ data.peers[0].reserved.join(',') }}]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</v-card>
|
||||
</template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.udp_timeout != undefined">
|
||||
<v-text-field
|
||||
label="UDP Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
:suffix="$t('date.m')"
|
||||
v-model.number="udp_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
|
||||
<v-text-field
|
||||
:label="$t('types.wg.worker')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
v-model.number="data.workers">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.mtu != undefined">
|
||||
<v-text-field
|
||||
label="MTU"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
v-model.number="data.mtu">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.system">
|
||||
<v-text-field
|
||||
:label="$t('types.wg.ifName')"
|
||||
hide-details
|
||||
v-model="ifName">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUdp" color="primary" label="UDP Timeout" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMtu" color="primary" label="MTU" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
computed: {
|
||||
optionUdp: {
|
||||
get(): boolean { return this.$props.data.udp_timeout != undefined },
|
||||
set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
|
||||
},
|
||||
optionWorker: {
|
||||
get(): boolean { return this.$props.data.workers != undefined },
|
||||
set(v:boolean) { this.$props.data.workers = v ? 2 : undefined }
|
||||
},
|
||||
optionMtu: {
|
||||
get(): boolean { return this.$props.data.mtu != undefined },
|
||||
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
|
||||
},
|
||||
ifName: {
|
||||
get() { return this.$props.data.name?? '' },
|
||||
set(v:string) { this.$props.data.name = v.length > 0 ? v : undefined }
|
||||
},
|
||||
udp_timeout: {
|
||||
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
|
||||
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<v-card subtitle="Wireguard">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
v-model="data.private_key"
|
||||
:label="$t('types.wg.privKey')"
|
||||
append-icon="mdi-key-star"
|
||||
@click:append="newKey()"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
v-model="public_key"
|
||||
readonly
|
||||
:label="$t('tls.pubKey')"
|
||||
append-icon="mdi-refresh"
|
||||
@click:append="getWgPubKey()"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('in.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
v-model.number="data.listen_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.udp_timeout != undefined">
|
||||
<v-text-field
|
||||
label="UDP Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
:suffix="$t('date.m')"
|
||||
v-model.number="udp_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
|
||||
<v-text-field
|
||||
:label="$t('types.wg.worker')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
v-model.number="data.workers">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.mtu != undefined">
|
||||
<v-text-field
|
||||
label="MTU"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
v-model.number="data.mtu">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="data.ext.dns" :label="$t('dns.title') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.system">
|
||||
<v-text-field
|
||||
:label="$t('types.wg.ifName')"
|
||||
hide-details
|
||||
v-model="ifName">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUdp" color="primary" label="UDP Timeout" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMtu" color="primary" label="MTU" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card v-if="data.peers != undefined">
|
||||
<v-card-subtitle>
|
||||
{{ $t('types.wg.peers') }}
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="addPeer"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-card-subtitle>
|
||||
<template v-for="(p, index) in data.peers">
|
||||
<v-card style="margin-top: 1rem;">
|
||||
<v-card-subtitle>
|
||||
{{ $t('types.wg.peer') + ' ' + (Number(index)+1) }} <v-icon color="error" icon="mdi-delete" @click="delPeer(Number(index))" />
|
||||
</v-card-subtitle>
|
||||
<Peer :data="p" :ext="data.ext" @refreshPeerKey="$emit('refreshPeerKey', index)" />
|
||||
</v-card>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Peer from '@/components/WgPeer.vue'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
emits: ['newWgKey', 'getWgPubKey', 'addPeer', 'delPeer', 'refreshPeerKey'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addPeer() {
|
||||
this.$emit('addPeer')
|
||||
},
|
||||
delPeer(id: number) {
|
||||
this.$emit('delPeer', id)
|
||||
},
|
||||
refreshPeerKey(id: number) {
|
||||
this.$emit('refreshPeerKey', id)
|
||||
},
|
||||
newKey() {
|
||||
this.$emit('newWgKey')
|
||||
},
|
||||
getWgPubKey() {
|
||||
const privKey = this.$props.data.private_key
|
||||
if (privKey.length == 0) return
|
||||
this.$emit('getWgPubKey', privKey)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
optionUdp: {
|
||||
get(): boolean { return this.$props.data.udp_timeout != undefined },
|
||||
set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
|
||||
},
|
||||
optionRsrv: {
|
||||
get(): boolean { return this.$props.data.reserved != undefined },
|
||||
set(v:boolean) { this.$props.data.reserved = v ? [0,0,0] : undefined }
|
||||
},
|
||||
optionWorker: {
|
||||
get(): boolean { return this.$props.data.workers != undefined },
|
||||
set(v:boolean) { this.$props.data.workers = v ? 2 : undefined }
|
||||
},
|
||||
optionMtu: {
|
||||
get(): boolean { return this.$props.data.mtu != undefined },
|
||||
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
|
||||
},
|
||||
ifName: {
|
||||
get() { return this.$props.data.name?? '' },
|
||||
set(v:string) { this.$props.data.name = v.length > 0 ? v : undefined }
|
||||
},
|
||||
address: {
|
||||
get() { return this.$props.data.address?.join(',') },
|
||||
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
reserved: {
|
||||
get() { return this.$props.data.reserved?.join(',') },
|
||||
set(v:string) {
|
||||
if(!v.endsWith(',')) {
|
||||
this.$props.data.reserved = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
|
||||
}
|
||||
}
|
||||
},
|
||||
udp_timeout: {
|
||||
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
|
||||
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
|
||||
},
|
||||
public_key: {
|
||||
get() { return this.$props.data.ext?.public_key?? '' },
|
||||
set(v:string) { this.$props.data.ext.public_key = v }
|
||||
}
|
||||
},
|
||||
components: { Peer }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<v-card subtitle="CCM (Claude Code Multiplexer)">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('types.ccm.credentialPath')"
|
||||
hide-details
|
||||
v-model="data.credential_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('types.ccm.usagesPath')"
|
||||
hide-details
|
||||
v-model="data.usages_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
:label="$t('dial.detourText')"
|
||||
hide-details
|
||||
:items="outTags"
|
||||
v-model="data.detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-title>
|
||||
{{ $t('types.ccm.users') }}
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="addUser"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-card-title>
|
||||
<v-card v-for="(user, index) in (data.users || [])" :key="index" class="border" style="margin: 4px; padding: 8px;" rounded="xl">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<v-icon @click="delUser(index)" color="error" icon="mdi-delete" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field :label="$t('types.ccm.userName')" hide-details v-model="user.name" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field :label="$t('types.ccm.userToken')" hide-details type="password" v-model="user.token" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Data from '@/store/modules/data'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
computed: {
|
||||
outTags() {
|
||||
return [...Data().outbounds?.map((o: any) => o.tag) ?? [], ...Data().endpoints?.map((e: any) => e.tag) ?? []]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addUser() {
|
||||
if (!this.$props.data.users) this.$props.data.users = []
|
||||
this.$props.data.users.push({ name: '', token: '' })
|
||||
},
|
||||
delUser(i: number | string) {
|
||||
this.$props.data.users?.splice(Number(i), 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<v-card subtitle="DERP">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
:label="$t('types.derp.configPath')"
|
||||
hide-details
|
||||
v-model="data.config_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionHome">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
:label="$t('pages.home')"
|
||||
hide-details
|
||||
placeholder="blank | http[s]://example.com:port/path"
|
||||
v-model="data.home">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionVerifyCE">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-select
|
||||
:label="$t('types.derp.verifyClientEndpoint')"
|
||||
hide-details
|
||||
:items="tsTags"
|
||||
multiple
|
||||
v-model="data.verify_client_endpoint">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="optionVerifyCU">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('types.derp.verifyClientUrl') }}</v-col>
|
||||
<v-col cols="auto" align-self="center" justify-self="center">
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="data.verify_client_url.push({url: ''})"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-card v-for="clientUrl, index in data.verify_client_url" :key="index" class="border" style="padding: 8px;" rounded="xl">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center" justify-self="center">
|
||||
<v-icon @click="data.verify_client_url.splice(index, 1)" color="error" icon="mdi-delete" />
|
||||
</v-col>
|
||||
<v-col cols="11">
|
||||
<v-text-field
|
||||
:label="$t('types.derp.verifyClientUrl')"
|
||||
hide-details
|
||||
v-model="clientUrl.url">
|
||||
</v-text-field>
|
||||
<Dial :dial="clientUrl" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
<template v-if="optionMesh">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('types.derp.meshWith') }}</v-col>
|
||||
<v-col cols="auto" align-self="center" justify-self="center">
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="data.mesh_with.push({tls: {}})"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-card v-for="mesh, index in data.mesh_with" :key="index" class="border" style="padding: 8px;" rounded="xl">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center" justify-self="center">
|
||||
<v-icon @click="data.mesh_with.splice(index, 1)" color="error" icon="mdi-delete" />
|
||||
</v-col>
|
||||
<v-col cols="11">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
v-model="mesh.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
v-model.number="mesh.server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.host')"
|
||||
hide-details
|
||||
v-model="mesh.host">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Dial :dial="mesh" />
|
||||
<OutTLS :outbound="mesh" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="usePskText"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="delete data.mesh_psk_file"
|
||||
>{{ $t('types.derp.meshPsk') }}</v-btn>
|
||||
<v-btn
|
||||
@click="delete data.mesh_psk"
|
||||
>{{ $t('types.derp.meshPskFile') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="usePskText == 1">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:label="$t('types.derp.meshPskFile')"
|
||||
hide-details
|
||||
v-model="data.mesh_psk_file">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:label="$t('types.derp.meshPsk')"
|
||||
hide-details
|
||||
v-model="data.mesh_psk">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="optionStun">
|
||||
<v-card :title="$t('types.derp.stun')" class="border" style="padding: 8px;" rounded="xl">
|
||||
<Listen :data="data.stun" :inTags="inTags" />
|
||||
</v-card>
|
||||
</template>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.derp.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionVerifyCE" color="primary" :label="$t('types.derp.verifyClientEndpoint')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionVerifyCU" color="primary" :label="$t('types.derp.verifyClientUrl')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionHome" color="primary" :label="$t('pages.home')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMesh" color="primary" :label="$t('types.derp.meshWith')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionStun" color="primary" :label="$t('types.derp.stun')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Dial from '@/components/Dial.vue'
|
||||
import OutTLS from '../tls/OutTLS.vue'
|
||||
import Listen from '../Listen.vue'
|
||||
export default {
|
||||
props: ['data', 'tsTags', 'inTags'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
usePskText: this.$props.data.mesh_psk == undefined ? 1 : 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionVerifyCE: {
|
||||
get() { return this.$props.data.verify_client_endpoint != undefined },
|
||||
set(v: boolean) { this.$props.data.verify_client_endpoint = v ? [] : undefined }
|
||||
},
|
||||
optionVerifyCU: {
|
||||
get() { return this.$props.data.verify_client_url != undefined },
|
||||
set(v: boolean) { this.$props.data.verify_client_url = v ? [{ url: '' }] : undefined }
|
||||
},
|
||||
optionHome: {
|
||||
get() { return this.$props.data.home != undefined },
|
||||
set(v: boolean) { this.$props.data.home = v ? '' : undefined }
|
||||
},
|
||||
optionMesh: {
|
||||
get() { return this.$props.data.mesh_with != undefined },
|
||||
set(v: boolean) {
|
||||
if (v) {
|
||||
this.$props.data.mesh_with = [{tls: {}}]
|
||||
delete this.$props.data.mesh_psk_file
|
||||
this.$props.data.mesh_psk = ''
|
||||
} else {
|
||||
delete this.$props.data.mesh_with
|
||||
delete this.$props.data.mesh_psk_file
|
||||
delete this.$props.data.mesh_psk
|
||||
}
|
||||
}
|
||||
},
|
||||
optionStun: {
|
||||
get() { return this.$props.data.stun != undefined },
|
||||
set(v: boolean) { this.$props.data.stun = v ? {enabled: true} : undefined }
|
||||
}
|
||||
},
|
||||
components: { Dial, Listen, OutTLS },
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<v-card subtitle="OCM (OpenAI Codex Multiplexer)">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('types.ocm.credentialPath')"
|
||||
hide-details
|
||||
v-model="data.credential_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('types.ocm.usagesPath')"
|
||||
hide-details
|
||||
v-model="data.usages_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
:label="$t('dial.detourText')"
|
||||
hide-details
|
||||
:items="outTags"
|
||||
v-model="data.detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-title>
|
||||
{{ $t('types.ocm.users') }}
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="addUser"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-card-title>
|
||||
<v-card v-for="(user, index) in (data.users || [])" :key="index" class="border" style="margin: 4px; padding: 8px;" rounded="xl">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<v-icon @click="delUser(index)" color="error" icon="mdi-delete" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field :label="$t('types.ocm.userName')" hide-details v-model="user.name" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field :label="$t('types.ocm.userToken')" hide-details type="password" v-model="user.token" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Data from '@/store/modules/data'
|
||||
|
||||
export default {
|
||||
props: ['data'],
|
||||
computed: {
|
||||
outTags() {
|
||||
return [...Data().outbounds?.map((o: any) => o.tag) ?? [], ...Data().endpoints?.map((e: any) => e.tag) ?? []]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addUser() {
|
||||
if (!this.$props.data.users) this.$props.data.users = []
|
||||
this.$props.data.users.push({ name: '', token: '' })
|
||||
},
|
||||
delUser(i: number | string) {
|
||||
this.$props.data.users?.splice(Number(i), 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<v-card style="padding: 8px;" rounded="xl" class="border">
|
||||
<v-card-subtitle>Shadowsocks API
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="add_server"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-card-subtitle>
|
||||
<v-row v-for="(server, index) in servers">
|
||||
<v-col cols="auto" align-self="center" justify-self="center">
|
||||
<v-icon @click="del_server(index)" color="error" icon="mdi-delete" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
@input="update_key(index,$event.target.value)"
|
||||
v-model="server.name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
:label="$t('objects.inbound')"
|
||||
hide-details
|
||||
:items="ssTags"
|
||||
@update:model-value="update_value(index,$event)"
|
||||
v-model="server.value">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
type Server = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
export default {
|
||||
props: ['data', 'ssTags'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
add_server() {
|
||||
this.servers = [...this.servers, {name: "/ss" + this.servers.length, value: this.ssTags[0] || ""}]
|
||||
},
|
||||
del_server(i:number) {
|
||||
let h = this.servers
|
||||
h.splice(i,1)
|
||||
this.servers = h
|
||||
},
|
||||
update_key(i:number,k:string) {
|
||||
let h = this.servers
|
||||
h[i].name = k
|
||||
this.servers = h
|
||||
},
|
||||
update_value(i:number,v:string) {
|
||||
let h = this.servers
|
||||
h[i].value = v
|
||||
this.servers = h
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
servers: {
|
||||
get() :Server[] {
|
||||
let servers: Server[] = []
|
||||
const h = this.$props.data.servers
|
||||
if (h) {
|
||||
Object.keys(h).forEach(key => {
|
||||
if (Array.isArray(h[key])){
|
||||
h[key].forEach((v:string) => servers.push({ name: key, value: v }))
|
||||
} else {
|
||||
servers.push({ name: key, value: h[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
return servers
|
||||
},
|
||||
set(v:Server[]) {
|
||||
if (v.length>0) {
|
||||
let servers:any = {}
|
||||
v.forEach((h:Server) => {
|
||||
if (servers[h.name]) {
|
||||
if (Array.isArray(servers[h.name])) {
|
||||
servers[h.name].push(h.value)
|
||||
} else {
|
||||
servers[h.name] = [servers[h.name], h.value]
|
||||
}
|
||||
} else {
|
||||
servers[h.name] = h.value
|
||||
}
|
||||
})
|
||||
this.$props.data.servers = servers
|
||||
} else {
|
||||
this.$props.data.servers = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup>
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
tilesData: <any>{},
|
||||
type: String
|
||||
})
|
||||
|
||||
const data = computed(() => {
|
||||
const d = props.tilesData
|
||||
if (!d.mem && !d.cpu) return { percent: 0, text: '-' }
|
||||
switch (props.type) {
|
||||
case 'g-cpu':
|
||||
return { percent: d.cpu, text: Math.ceil(d.cpu) + "%" }
|
||||
case 'g-mem':
|
||||
return gaugeData(d.mem)
|
||||
case 'g-dsk':
|
||||
return gaugeData(d.dsk)
|
||||
case 'g-swp':
|
||||
return gaugeData(d.swp)
|
||||
}
|
||||
return { percent: 0, text: '-'}
|
||||
})
|
||||
|
||||
const gaugeData = (d:any) :any => {
|
||||
if (!d) return { percent: 0, text: '-' }
|
||||
const curr = HumanReadable.sizeFormat(d.current,0).split(' ')
|
||||
const total = HumanReadable.sizeFormat(d.total,0).split(' ')
|
||||
if (curr[1] == total[1]) curr[1] = ''
|
||||
return {
|
||||
percent: Math.ceil(d.current*100/d.total),
|
||||
text: curr[0] + "<sup>" + (curr[1]?? ' ') + "</sup>/" + total[0] + "<sup>" + (total[1]?? '') + "</sup>"
|
||||
}
|
||||
}
|
||||
|
||||
const cssTransformRotateValue = computed(() => {
|
||||
const percentageAsFraction = data.value.percent / 100
|
||||
const halfPercentage = percentageAsFraction / 2
|
||||
|
||||
return `${halfPercentage}turn`
|
||||
})
|
||||
|
||||
const gaugeColor = computed(() => {
|
||||
if (data.value.percent > 90) return 'error'
|
||||
if (data.value.percent > 70) return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gauge__outer">
|
||||
<div class="gauge__inner">
|
||||
<div
|
||||
class="gauge__fill"
|
||||
:style="{
|
||||
transform: `rotate(${cssTransformRotateValue})`,
|
||||
background: `rgb(var(--v-theme-${gaugeColor}))`
|
||||
}">
|
||||
</div>
|
||||
<div class="gauge__cover"><span dir="ltr" v-html="data.text"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gauge__outer {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.gauge__inner {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 50%;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
position: relative;
|
||||
border-top-left-radius: 100% 200%;
|
||||
border-top-right-radius: 100% 200%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gauge__fill {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: inherit;
|
||||
height: 100%;
|
||||
background: rgb(var(--v-theme-primary));
|
||||
transform-origin: center top;
|
||||
transform: rotate(0turn);
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.gauge__cover {
|
||||
width: 75%;
|
||||
height: 150%;
|
||||
background: rgb(var(--v-theme-background));
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50%;
|
||||
|
||||
/* Text */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 25%;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Lexend', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
sup {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<Line v-if="loaded" :data="data" :options="<any>options" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Filler
|
||||
)
|
||||
ChartJS.defaults.font.family = 'Vazirmatn'
|
||||
export default {
|
||||
components: {
|
||||
Line
|
||||
},
|
||||
props: ['tilesData','type'],
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
labels: new Array(20).fill(''),
|
||||
oldValues: <any>{net: {}, dio: {}},
|
||||
options1: {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: {
|
||||
color: '#777777',
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
steps: 10,
|
||||
stepValue: 5,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
optionsNet: {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: '#777777',
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (label:any, index: number) => { return parseInt(label).toString() },
|
||||
count: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: ref(<any>{})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
switch (this.$props.type){
|
||||
case "h-net":
|
||||
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
|
||||
return label == 0 ? "0" : HumanReadable.sizeFormat(label,0)
|
||||
}
|
||||
return this.optionsNet
|
||||
case "hp-net":
|
||||
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
|
||||
return label == 0 ? "0" : HumanReadable.packetFormat(label,0)
|
||||
}
|
||||
return this.optionsNet
|
||||
case "h-dio":
|
||||
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
|
||||
return label == 0 ? "0" : HumanReadable.sizeFormat(label,0)
|
||||
}
|
||||
return this.optionsNet
|
||||
}
|
||||
return this.options1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData1(value1: number) {
|
||||
const newData = <number[]>[]
|
||||
if (this.data.datasets){
|
||||
newData.push(...this.data.datasets[0].data,value1)
|
||||
}
|
||||
if (newData.length>20) newData.shift()
|
||||
this.data = {
|
||||
labels: this.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.2)',
|
||||
borderColor: 'rgba(255, 165, 0,0.8)',
|
||||
fill: true,
|
||||
data: newData
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
},
|
||||
updateData2(value1: number, value2:number) {
|
||||
const newData1 = <number[]>[]
|
||||
const newData2 = <number[]>[]
|
||||
if (this.data.datasets){
|
||||
newData1.push(...this.data.datasets[0].data,value1)
|
||||
newData2.push(...this.data.datasets[1].data,value2)
|
||||
}
|
||||
if (newData1.length>20) newData1.shift()
|
||||
if (newData2.length>20) newData2.shift()
|
||||
this.data = {
|
||||
labels: this.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.2)',
|
||||
borderColor: 'rgba(255, 165, 0,0.8)',
|
||||
fill: true,
|
||||
data: newData1
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
backgroundColor: 'rgba(0, 128, 0, 0.1)',
|
||||
borderColor: 'rgba(0, 128, 0,0.8)',
|
||||
fill: true,
|
||||
data: newData2
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tilesData(v:any) {
|
||||
switch (this.$props.type) {
|
||||
case 'h-cpu':
|
||||
this.updateData1(v.cpu)
|
||||
break
|
||||
case 'h-mem':
|
||||
this.updateData1(v.mem.current*100/v.mem.total)
|
||||
break
|
||||
case 'h-net':
|
||||
if (this.oldValues.net.sent) {
|
||||
const downSpeed = (v.net.recv-this.oldValues.net.recv)/2 // Each 2 sec
|
||||
const upSpeed = (v.net.sent-this.oldValues.net.sent)/2 // Each 2 sec
|
||||
this.updateData2(upSpeed,downSpeed)
|
||||
}
|
||||
this.oldValues.net = v.net
|
||||
break
|
||||
case 'hp-net':
|
||||
if (this.oldValues.net.psent) {
|
||||
const downSpeed = (v.net.precv-this.oldValues.net.precv)/2 // Each 2 sec
|
||||
const upSpeed = (v.net.psent-this.oldValues.net.psent)/2 // Each 2 sec
|
||||
this.updateData2(upSpeed,downSpeed)
|
||||
}
|
||||
this.oldValues.net = v.net
|
||||
break
|
||||
case 'h-dio':
|
||||
if (this.oldValues.dio.read) {
|
||||
const downSpeed = (v.dio.read-this.oldValues.dio.read)/2 // Each 2 sec
|
||||
const upSpeed = (v.dio.write-this.oldValues.dio.write)/2 // Each 2 sec
|
||||
this.updateData2(upSpeed,downSpeed)
|
||||
}
|
||||
this.oldValues.dio = v.dio
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<v-card subtitle="ACME" style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" v-if="enabled">
|
||||
<v-text-field
|
||||
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="domains">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="enabled">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDir">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.dataDir')"
|
||||
hide-details
|
||||
v-model="acme.data_directory">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
|
||||
<v-combobox
|
||||
v-model="acme.default_server_name"
|
||||
:items="acme.domain"
|
||||
:label="$t('tls.acme.defaultDomain')"
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
|
||||
<v-text-field
|
||||
:label="$t('email')"
|
||||
hide-details
|
||||
v-model="acme.email">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionChallenge">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionPorts">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.altHport')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
max="65532"
|
||||
v-model.number="acme.alternative_http_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.altTport')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
max="65532"
|
||||
v-model.number="acme.alternative_tls_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionProvider">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="caProvider"
|
||||
:items="providerList"
|
||||
:label="$t('tls.acme.caProvider')"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" v-if="caProvider == ''">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.customCa')"
|
||||
hide-details
|
||||
v-model="acme.provider">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="acme.external_account != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Key ID"
|
||||
hide-details
|
||||
v-model="acme.external_account.key_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="MAC Key"
|
||||
hide-details
|
||||
v-model="acme.external_account.mac_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="acme.dns01_challenge != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
:label="$t('tls.acme.dns01Provider')"
|
||||
hide-details
|
||||
:items="dnsProviders.map(d => d.provider)"
|
||||
@update:model-value="acme.dns01_challenge = { provider: $event }"
|
||||
v-model="acme.dns01_challenge.provider">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4"
|
||||
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
|
||||
:key="item">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.dns01Params.' + item)"
|
||||
hide-details
|
||||
v-model="acme.dns01_challenge[item]">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.acme.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDir" color="primary" :label="$t('tls.acme.dataDir')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDefault" color="primary" :label="$t('tls.acme.defaultDomain')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionEmail" color="primary" :label="$t('email')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionChallenge" color="primary" :label="$t('tls.acme.disableChallenges')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionPorts" color="primary" :label="$t('tls.acme.altPorts')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionProvider" color="primary" :label="$t('tls.acme.caProvider')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionExt" color="primary" :label="$t('tls.acme.extAcc')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDns01" color="primary" :label="$t('tls.acme.dns01')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { acme } from '@/types/tls'
|
||||
|
||||
export default {
|
||||
props: ['tls'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
providerList: [
|
||||
{ title: "Let's Encrypt", value: "letsencrypt" },
|
||||
{ title: "ZeroSSL", value: "zerossl" },
|
||||
{ title: "Custom", value: "" }
|
||||
],
|
||||
dnsProviders: [
|
||||
{ provider: "cloudflare", params: [ "api_token", "zone_token" ] },
|
||||
{ provider: "alidns", params: [ "access_key_id", "access_key_secret", "region_id", "security_token" ] },
|
||||
{ provider: "acmedns", params: [ "username", "password", "subdomain", "server_url" ] }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
acme() {
|
||||
return <acme>this.$props.tls.acme
|
||||
},
|
||||
enabled: {
|
||||
get() { return this.acme != undefined },
|
||||
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
|
||||
},
|
||||
domains: {
|
||||
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
|
||||
set(v: string) {
|
||||
if(!v.endsWith(',')) {
|
||||
this.acme.domain = v.length > 0 ? v.split(',') : []
|
||||
}
|
||||
}
|
||||
},
|
||||
caProvider: {
|
||||
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
|
||||
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
|
||||
},
|
||||
optionDir: {
|
||||
get(): boolean { return this.acme?.data_directory != undefined },
|
||||
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
|
||||
},
|
||||
optionDefault: {
|
||||
get(): boolean { return this.acme?.default_server_name != undefined },
|
||||
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
|
||||
},
|
||||
optionEmail: {
|
||||
get(): boolean { return this.acme?.email != undefined },
|
||||
set(v:boolean) { this.acme.email = v ? '' : undefined }
|
||||
},
|
||||
optionChallenge: {
|
||||
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.acme.disable_http_challenge = false
|
||||
this.acme.disable_tls_alpn_challenge = false
|
||||
} else {
|
||||
delete this.acme.disable_http_challenge
|
||||
delete this.acme.disable_tls_alpn_challenge
|
||||
}
|
||||
}
|
||||
},
|
||||
optionPorts: {
|
||||
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.acme.alternative_http_port = 80
|
||||
this.acme.alternative_tls_port = 443
|
||||
} else {
|
||||
delete this.acme.alternative_http_port
|
||||
delete this.acme.alternative_tls_port
|
||||
}
|
||||
}
|
||||
},
|
||||
optionProvider: {
|
||||
get(): boolean { return this.acme?.provider != undefined },
|
||||
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
|
||||
},
|
||||
optionExt: {
|
||||
get(): boolean { return this.acme?.external_account != undefined },
|
||||
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
|
||||
},
|
||||
optionDns01: {
|
||||
get(): boolean { return this.acme?.dns01_challenge != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.acme.dns01_challenge = { provider: 'cloudflare' }
|
||||
} else {
|
||||
delete this.acme.dns01_challenge
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<v-card subtitle="ECH" style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="enabled">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="useEchPath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="delete ech.key"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="delete ech.key_path"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="mdi-key-star"
|
||||
@click="genECH"
|
||||
:loading="loading">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.generate') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="useEchPath == 0">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
v-model="ech.key_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
v-model="echKeyText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" v-if="oTls.ech">
|
||||
<v-text-field
|
||||
:label="$t('tls.queryServerName')"
|
||||
hide-details
|
||||
v-model="oTls.ech.query_server_name"
|
||||
placeholder="ech.example.com">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="echConfigText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { ech } from '@/types/tls'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['iTls','oTls'],
|
||||
data() {
|
||||
return {
|
||||
useEchPath: this.$props.iTls?.ech?.key? 1:0,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async genECH(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', {
|
||||
k: "ech",
|
||||
o: this.iTls.server_name?? "''"
|
||||
})
|
||||
this.loading = false
|
||||
if (msg.success && this.iTls.ech && this.oTls.ech) {
|
||||
this.iTls.ech.key_path=undefined
|
||||
this.useEchPath = 1
|
||||
if (msg.obj.length>0){
|
||||
let config = <string[]>[]
|
||||
let key = <string[]>[]
|
||||
let isConfig = false
|
||||
let isKey = false
|
||||
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (line === "-----BEGIN ECH CONFIGS-----") {
|
||||
isConfig = true
|
||||
isKey = false
|
||||
config.push(line)
|
||||
} else if (line === "-----END ECH CONFIGS-----") {
|
||||
isConfig = false
|
||||
config.push(line)
|
||||
} else if (line === "-----BEGIN ECH KEYS-----") {
|
||||
isKey = true
|
||||
isConfig = false
|
||||
key.push(line)
|
||||
} else if (line === "-----END ECH KEYS-----") {
|
||||
isKey = false
|
||||
key.push(line)
|
||||
} else if (isConfig) {
|
||||
config.push(line)
|
||||
} else if (isKey) {
|
||||
key.push(line)
|
||||
}
|
||||
})
|
||||
this.iTls.ech.key = key?? undefined
|
||||
this.oTls.ech.config = config?? undefined
|
||||
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
ech() {
|
||||
return <ech>this.$props.iTls.ech
|
||||
},
|
||||
enabled: {
|
||||
get() { return this.ech?.enabled?? false },
|
||||
set(v: boolean) {
|
||||
this.$props.iTls.ech = v ? { enabled: true } : undefined
|
||||
this.$props.oTls.ech = v ? {} : undefined
|
||||
}
|
||||
},
|
||||
echKeyText: {
|
||||
get(): string { return this.ech?.key ? this.ech.key.join('\n') : '' },
|
||||
set(newValue:string) { this.ech.key = newValue.split('\n') }
|
||||
},
|
||||
echConfigText: {
|
||||
get(): string { return this.oTls.ech?.config ? this.oTls.ech.config.join('\n') : '' },
|
||||
set(newValue:string) { this.oTls.ech.config = newValue.split('\n') }
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.tls')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('template')"
|
||||
:items="tlsItems"
|
||||
v-model="inbound.tls_id">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
export default {
|
||||
props: ['inbound', 'tlsConfigs'],
|
||||
computed: {
|
||||
tlsItems(): any[] {
|
||||
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.tls')">
|
||||
<v-row v-if="tlsOptional">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="tls.enabled">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.disableSni')" v-model="disable_sni" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.insecure')" v-model="insecure" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="optionCert">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="usePath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="tls.certificate=undefined; tls.certificate_path=''"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="tls.certificate_path=undefined; tls.certificate=''"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="usePath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.certPath')"
|
||||
hide-details
|
||||
v-model="tls.certificate_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="tls.certificate">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
|
||||
<v-text-field
|
||||
label="SNI"
|
||||
hide-details
|
||||
v-model="tls.server_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
|
||||
<v-select
|
||||
hide-details
|
||||
label="ALPN"
|
||||
multiple
|
||||
:items="alpn"
|
||||
v-model="tls.alpn">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.minVer')"
|
||||
:items="tlsVersions"
|
||||
v-model="tls.min_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.maxVer')"
|
||||
:items="tlsVersions"
|
||||
v-model="tls.max_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="tls.cipher_suites != undefined">
|
||||
<v-col cols="12" md="8">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.cs')"
|
||||
multiple
|
||||
:items="cipher_suites"
|
||||
v-model="tls.cipher_suites">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="tls.utls != undefined">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Fingerprint"
|
||||
:items="fingerprints"
|
||||
v-model="tls.utls.fingerprint">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="tls.reality != undefined">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.pubKey')"
|
||||
hide-details
|
||||
v-model="tls.reality.public_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
label="Short ID"
|
||||
hide-details
|
||||
v-model="tls.reality.short_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="tls.ech != undefined">
|
||||
<v-row>
|
||||
<v-col class="v-card-subtitle">ECH</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="useEchPath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="delete tls.ech?.config"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="delete tls.ech?.config_path"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="useEchPath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.certPath')"
|
||||
hide-details
|
||||
v-model="tls.ech.config_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.queryServerName')"
|
||||
hide-details
|
||||
v-model="tls.ech.query_server_name"
|
||||
placeholder="ech.example.com">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="echConfigText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.queryServerName')"
|
||||
hide-details
|
||||
v-model="tls.ech.query_server_name"
|
||||
placeholder="ech.example.com">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-row v-if="tls.fragment != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.fragment')" v-model="tls.fragment" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.fragment">
|
||||
<v-switch color="primary" :label="$t('tls.recordFragment')" v-model="tls.record_fragment" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.fragment">
|
||||
<v-text-field
|
||||
:label="$t('tls.fragmentDelay')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=0
|
||||
:suffix="$t('date.ms')"
|
||||
v-model.number="fragmentFallbackDelay">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-card-actions v-if="tls.enabled">
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCert" color="primary" :label="$t('tls.cert')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionReality" color="primary" label="Reality" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionEch" color="primary" label="ECH" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionFragment" color="primary" :label="$t('tls.fragment')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { oTls, defaultOutTls } from '@/types/tls'
|
||||
export default {
|
||||
props: ['outbound'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
usePath: this.$props.outbound?.tls?.certificate? 1:0,
|
||||
useEchPath: this.$props.outbound?.tls.ech?.config? 1:0,
|
||||
defaults: defaultOutTls,
|
||||
alpn: [
|
||||
{ title: "H3", value: 'h3' },
|
||||
{ title: "H2", value: 'h2' },
|
||||
{ title: "Http/1.1", value: 'http/1.1' },
|
||||
],
|
||||
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
|
||||
cipher_suites: [
|
||||
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
|
||||
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
|
||||
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
|
||||
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
|
||||
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
|
||||
],
|
||||
fingerprints: [
|
||||
{ title: "Chrome", value: "chrome" },
|
||||
{ title: "Firefox", value: "firefox" },
|
||||
{ title: "Microsoft Edge", value: "edge" },
|
||||
{ title: "Apple Safari", value: "safari" },
|
||||
{ title: "360", value: "360" },
|
||||
{ title: "QQ", value: "qq" },
|
||||
{ title: "Apple IOS", value: "ios" },
|
||||
{ title: "Android", value: "android" },
|
||||
{ title: "Random", value: "random" },
|
||||
{ title: "Randomized", value: "randomized" },
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tls(): oTls {
|
||||
return <oTls> this.$props.outbound.tls
|
||||
},
|
||||
tlsEnable: {
|
||||
get() { return Object.hasOwn(this.tls, 'enabled') ? this.tls.enabled : false },
|
||||
set(newValue: boolean) { this.$props.outbound.tls = newValue ? { enabled: true } : { enabled: false } }
|
||||
},
|
||||
disable_sni: {
|
||||
get() { return this.tls.disable_sni ?? false },
|
||||
set(newValue: boolean) { this.$props.outbound.tls.disable_sni = newValue ? true : undefined }
|
||||
},
|
||||
insecure: {
|
||||
get() { return this.tls.insecure ?? false },
|
||||
set(newValue: boolean) { this.$props.outbound.tls.insecure = newValue ? true : undefined }
|
||||
},
|
||||
tlsOptional(): boolean {
|
||||
return !['hysteria','hysteria2','tuic','shadowtls', 'anytls', 'naive'].includes(this.$props.outbound.type)
|
||||
},
|
||||
echConfigText: {
|
||||
get(): string { return this.tls.ech?.config ? this.tls.ech.config.join('\n') : '' },
|
||||
set(newValue:string) { if (this.tls.ech) this.tls.ech.config = newValue.split('\n') }
|
||||
},
|
||||
optionCert: {
|
||||
get(): boolean { return this.tls.certificate != undefined || this.tls.certificate_path != undefined },
|
||||
set(v:boolean) {
|
||||
this.usePath = 0
|
||||
if (v) {
|
||||
this.$props.outbound.tls.certificate_path = ""
|
||||
} else {
|
||||
delete this.$props.outbound.tls.certificate_path
|
||||
delete this.$props.outbound.tls.certificate
|
||||
}
|
||||
}
|
||||
},
|
||||
optionSNI: {
|
||||
get(): boolean { return this.tls.server_name != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.server_name = v ? '' : undefined }
|
||||
},
|
||||
optionALPN: {
|
||||
get(): boolean { return this.tls.alpn != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.alpn = v ? defaultOutTls.alpn : undefined }
|
||||
},
|
||||
optionMinV: {
|
||||
get(): boolean { return this.tls.min_version != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.min_version = v ? defaultOutTls.min_version : undefined }
|
||||
},
|
||||
optionMaxV: {
|
||||
get(): boolean { return this.tls.max_version != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.max_version = v ? defaultOutTls.max_version : undefined }
|
||||
},
|
||||
optionCS: {
|
||||
get(): boolean { return this.tls.cipher_suites != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.cipher_suites = v ? defaultOutTls.cipher_suites : undefined }
|
||||
},
|
||||
optionFP: {
|
||||
get(): boolean { return this.tls.utls != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.utls = v ? defaultOutTls.utls : undefined }
|
||||
},
|
||||
optionReality: {
|
||||
get(): boolean { return this.tls.reality != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.reality = v ? defaultOutTls.reality : undefined }
|
||||
},
|
||||
optionEch: {
|
||||
get(): boolean { return this.tls.ech != undefined },
|
||||
set(v:boolean) { this.$props.outbound.tls.ech = v ? defaultOutTls.ech : undefined }
|
||||
},
|
||||
optionFragment: {
|
||||
get(): boolean { return this.tls.fragment != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.outbound.tls.fragment = false
|
||||
} else {
|
||||
delete this.$props.outbound.tls.fragment
|
||||
delete this.$props.outbound.tls.fragment_fallback_delay
|
||||
delete this.$props.outbound.tls.record_fragment
|
||||
}
|
||||
}
|
||||
},
|
||||
fragmentFallbackDelay: {
|
||||
get(): number { return parseInt(this.tls.fragment_fallback_delay?.replace('ms','')?? '500')?? 500 },
|
||||
set(v:number) { this.$props.outbound.tls.fragment_fallback_delay = v>0 ? `${v}ms` : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.hosts')"
|
||||
hide-details
|
||||
v-model="hosts">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="transport.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
:label="$t('transport.httpMethod')"
|
||||
hide-details
|
||||
clearable
|
||||
@click:clear="delete transport.method"
|
||||
:items="methodList"
|
||||
v-model="transport.method">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.idleTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="idle_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.pingTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="ping_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="transport" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { HTTP } from '../../types/transport'
|
||||
import Headers from '../Headers.vue'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
methodList: ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Http(): HTTP {
|
||||
return <HTTP> this.$props.transport?? {}
|
||||
},
|
||||
hosts: {
|
||||
get() { return this.Http.host ? this.Http.host.join(',') : '' },
|
||||
set(newValue:string) { this.$props.transport.host = newValue.length>0 ? newValue.split(',') : [] }
|
||||
},
|
||||
idle_timeout: {
|
||||
get() { return this.Http.idle_timeout ? parseInt(this.Http.idle_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.idle_timeout = newValue ? newValue + 's' : '' }
|
||||
},
|
||||
ping_timeout: {
|
||||
get() { return this.Http.ping_timeout ? parseInt(this.Http.ping_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' }
|
||||
}
|
||||
},
|
||||
components: { Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.hosts')"
|
||||
hide-details
|
||||
v-model="transport.host">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="transport.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="transport" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Headers from '../Headers.vue'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
components: { Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="transport.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.host')"
|
||||
hide-details
|
||||
v-model="host">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Max Early Data"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="max_early_data">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Early Data Header Name"
|
||||
hide-details
|
||||
v-model="transport.early_data_header_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Headers :data="transport" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { WebSocket } from '../../types/transport'
|
||||
import Headers from '../Headers.vue'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
WS(): WebSocket {
|
||||
return <WebSocket> this.$props.transport
|
||||
},
|
||||
max_early_data: {
|
||||
get() { return this.WS.max_early_data ? this.WS.max_early_data : '' },
|
||||
set(newValue:number) { this.$props.transport.max_early_data = newValue != 0 ? newValue : undefined }
|
||||
},
|
||||
host: {
|
||||
get() { return this.WS.headers?.Host ? this.WS.headers.Host : '' },
|
||||
set(newValue:string) {
|
||||
this.$props.transport.headers = newValue != "" ? { Host: newValue } : undefined
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.WS.early_data_header_name ??= 'Sec-WebSocket-Protocol'
|
||||
this.WS.path ??= '/'
|
||||
},
|
||||
components: { Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.grpcServiceName')"
|
||||
hide-details
|
||||
v-model="transport.service_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch
|
||||
color="primary"
|
||||
v-model="transport.permit_without_stream"
|
||||
:label="$t('transport.grpcPws')"
|
||||
hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.idleTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="idle_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.pingTimeout')"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="ping_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { gRPC } from '../../types/transport'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
GRPC(): gRPC {
|
||||
return <gRPC> this.$props.transport?? {}
|
||||
},
|
||||
idle_timeout: {
|
||||
get() { return this.GRPC.idle_timeout ? parseInt(this.GRPC.idle_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.idle_timeout = newValue ? newValue + 's' : '' }
|
||||
},
|
||||
ping_timeout: {
|
||||
get() { return this.GRPC.ping_timeout ? parseInt(this.GRPC.ping_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<v-app-bar :elevation="5">
|
||||
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
|
||||
<span v-else style="width: 24px"></span>
|
||||
<v-app-bar-title :text="$t(<string>route.name)" class="align-center text-center " />
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon v-bind="props">
|
||||
<v-icon>mdi-translate</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="lang in languages"
|
||||
:key="lang.value"
|
||||
@click="changeLocale(lang.value)"
|
||||
:active="isActiveLocale(lang.value)"
|
||||
>
|
||||
<v-list-item-title>{{ lang.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon v-bind="props">
|
||||
<v-icon>mdi-theme-light-dark</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="th in themes"
|
||||
:key="th.value"
|
||||
@click="changeTheme(th.value)"
|
||||
:prepend-icon="th.icon"
|
||||
:active="isActiveTheme(th.value)"
|
||||
>
|
||||
<v-list-item-title>{{ $t(`theme.${th.value}`) }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useLocale, useTheme } from 'vuetify'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { languages } from '@/locales'
|
||||
|
||||
defineProps(['isMobile'])
|
||||
|
||||
const route = useRoute()
|
||||
const { locale: i18nLocale } = useI18n()
|
||||
const vuetifyLocale = useLocale()
|
||||
const theme = useTheme()
|
||||
|
||||
const changeLocale = (l: string) => {
|
||||
i18nLocale.value = l
|
||||
vuetifyLocale.current.value = l
|
||||
localStorage.setItem('locale', l)
|
||||
window.location.reload()
|
||||
}
|
||||
const isActiveLocale = (l: string) => i18nLocale.value === l
|
||||
const themes = [
|
||||
{ value: 'light', icon: 'mdi-white-balance-sunny' },
|
||||
{ value: 'dark', icon: 'mdi-moon-waning-crescent' },
|
||||
{ value: 'system', icon: 'mdi-laptop' },
|
||||
]
|
||||
|
||||
const changeTheme = (th: string) => {
|
||||
theme.change(th)
|
||||
localStorage.setItem('theme', th)
|
||||
}
|
||||
const isActiveTheme = (th: string) => {
|
||||
const current = localStorage.getItem('theme') ?? 'system'
|
||||
return current == th
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<v-app style="overflow: auto;">
|
||||
<drawer :isMobile="isMobile" :displayDrawer="displayDrawer" @toggleDrawer="toggleDrawer" />
|
||||
<default-bar :isMobile="isMobile" @toggleDrawer="toggleDrawer" />
|
||||
<default-view />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import DefaultBar from './AppBar.vue'
|
||||
import Drawer from './Drawer.vue'
|
||||
import DefaultView from './View.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { smAndDown } = useDisplay()
|
||||
const displayDrawer = ref(false)
|
||||
|
||||
const toggleDrawer = () => {
|
||||
displayDrawer.value = !displayDrawer.value
|
||||
}
|
||||
|
||||
const isMobile = computed( ():boolean =>{
|
||||
displayDrawer.value = !smAndDown.value
|
||||
return smAndDown.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-card-subtitle {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid gray;
|
||||
min-height: 20px;
|
||||
}
|
||||
.v-switch.v-input {
|
||||
padding-inline-start: .6rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="showDrawer"
|
||||
:temporary="isMobile"
|
||||
:expand-on-hover="!isMobile"
|
||||
:rail="!isMobile"
|
||||
:permanent="!isMobile"
|
||||
@click="isMobile ? $emit('toggleDrawer') : null"
|
||||
>
|
||||
<v-list-item
|
||||
height="63"
|
||||
prepend-avatar="@/assets/logo.svg"
|
||||
title="S-UI"
|
||||
>
|
||||
<template v-slot:append v-if="isMobile">
|
||||
<v-icon icon="mdi-close" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item link
|
||||
v-for="item in menu"
|
||||
:key="item.title"
|
||||
:to="item.path"
|
||||
:active="router.currentRoute.value.path == item.path">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title v-text="$t(item.title)"></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<template v-slot:append>
|
||||
<v-list-item prepend-icon="mdi-logout" :title="$t('menu.logout')" @click="Logout"></v-list-item>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import router from '@/router'
|
||||
import { logout } from '@/plugins/httputil'
|
||||
|
||||
const props = defineProps(['isMobile','displayDrawer'])
|
||||
|
||||
const showDrawer = computed((): boolean => {
|
||||
return props.displayDrawer
|
||||
})
|
||||
|
||||
const menu = [
|
||||
{ title: 'pages.home', icon: 'mdi-home', path: '/' },
|
||||
{ title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' },
|
||||
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
|
||||
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
|
||||
{ title: 'pages.endpoints', icon: 'mdi-cloud-tags', path: '/endpoints' },
|
||||
{ title: 'pages.services', icon: 'mdi-server', path: '/services' },
|
||||
{ title: 'pages.tls', icon: 'mdi-certificate', path: '/tls' },
|
||||
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
|
||||
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
|
||||
{ title: 'pages.dns', icon: 'mdi-dns', path: '/dns' },
|
||||
{ title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' },
|
||||
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
|
||||
]
|
||||
|
||||
const Logout = async () => {
|
||||
logout()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-main {
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="400">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('admin.changeCred') + " " + user.username }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="newData.oldPass" :label="$t('admin.oldPass')" :rules="passwordRules" type="password" required></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="newData.newUsername" :label="$t('admin.newUname')" :rules="usernameRules" required></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="newData.newPass" :label="$t('admin.newPass')" :rules="passwordRules" type="password" required></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'user'],
|
||||
data() {
|
||||
return {
|
||||
newData: {
|
||||
id: 0,
|
||||
oldPass: "",
|
||||
newUsername: "",
|
||||
newPass: ""
|
||||
},
|
||||
usernameRules: [
|
||||
(value: string) => {
|
||||
if (value?.length > 0) return true
|
||||
return i18n.global.t('login.unRules')
|
||||
},
|
||||
],
|
||||
passwordRules: [
|
||||
(value: string) => {
|
||||
if (value?.length > 0) return true
|
||||
return i18n.global.t('login.pwRules')
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetData() {
|
||||
this.newData.id = this.$props.user.id
|
||||
this.newData.oldPass = ""
|
||||
this.newData.newUsername = ""
|
||||
this.newData.newPass = ""
|
||||
},
|
||||
closeModal() {
|
||||
this.resetData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
if (this.newData.oldPass == '' || this.newData.newUsername == '' || this.newData.newPass == '') return
|
||||
this.$emit('save', this.newData)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.resetData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="500">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('main.backup.title') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-icon icon="mdi-close" @click="control.visible = false" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-checkbox v-model="exclude" :label="$t('main.backup.exclStats')" value="stats" hide-details></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-checkbox v-model="exclude" :label="$t('main.backup.exclChanges')" value="changes" hide-details></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<v-btn color="primary" @click="backup()" hide-details>{{ $t('main.backup.backup') }}</v-btn>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<v-btn color="primary" @click="restore()" hide-details>{{ $t('main.backup.restore') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-divider></v-divider>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<v-btn color="primary" @click="config()" hide-details>{{ $t('main.backup.sbConfig') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
export default {
|
||||
props: ['control', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
exclude: ["stats", "changes"],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
backup() {
|
||||
const excludeOption = this.exclude.length>0 ? '?exclude=' +this.exclude.join(',') : ''
|
||||
window.location.href = 'api/getdb' + excludeOption
|
||||
},
|
||||
config() {
|
||||
window.location.href = 'api/singbox-config'
|
||||
},
|
||||
restore() {
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.db'
|
||||
|
||||
fileInput.addEventListener('change', async (event: Event) => {
|
||||
const inputElement = event.target as HTMLInputElement
|
||||
const dbFile = inputElement.files ? inputElement.files[0] : null
|
||||
|
||||
if (dbFile) {
|
||||
const formData = new FormData()
|
||||
formData.append('db', dbFile)
|
||||
|
||||
this.control.visible = false
|
||||
|
||||
const uploadMsg = await HttpUtils.post('api/importdb', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
|
||||
if (uploadMsg.success) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fileInput.click()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.exclude = ["stats", "changes"]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="800" :loading="loading">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('admin.changes') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('admin.actor')"
|
||||
:items="['', 'DepleteJob', ...admins]"
|
||||
v-model="user"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('admin.key')"
|
||||
:items="['', 'inbounds', 'outbounds', 'clients', 'route', 'tls', 'experimental']"
|
||||
v-model="key"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('count')"
|
||||
:items="[10,20,30,50,100]"
|
||||
v-model.number="chngCount"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="auto" align="center" justify="center">
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="loadData">
|
||||
<v-icon />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-data-table
|
||||
:headers="changesHeaders"
|
||||
:items="changes"
|
||||
item-value="id"
|
||||
density="compact"
|
||||
show-expand
|
||||
items-per-page="10"
|
||||
>
|
||||
<template v-slot:item.dateTime="{ value }">
|
||||
<v-chip variant="text" dir="ltr" density="compact">
|
||||
{{ dateFormatted(value) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.action="{ value }">
|
||||
<v-chip density="compact">
|
||||
{{ $t('actions.' + value) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:expanded-row="{ columns, item }">
|
||||
<tr>
|
||||
<td :colspan="columns.length">
|
||||
<v-card dir="ltr" v-if="item.index>0">Index: {{ item.index }}</v-card>
|
||||
<v-card style="background-color: background" dir="ltr"><pre>{{ item.obj }}</pre></v-card>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
|
||||
export default {
|
||||
props: ['admins', 'actor', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
changes: <any[]>[],
|
||||
user: '',
|
||||
key: '',
|
||||
chngCount: 10,
|
||||
expanded: [],
|
||||
changesHeaders: [
|
||||
{ title: 'ID', key: 'id' },
|
||||
{ title: i18n.global.t('admin.date') + '-' + i18n.global.t('admin.time'), key: 'dateTime' },
|
||||
{ title: i18n.global.t('admin.actor'), key: 'Actor' },
|
||||
{ title: i18n.global.t('admin.key'), key: 'key' },
|
||||
{ title: i18n.global.t('admin.action'), key: 'action' },
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/changes',{ a: this.user, k: this.key, c: this.chngCount })
|
||||
if (data.success) {
|
||||
this.changes = data.obj?? []
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
dateFormatted(dt: number): string {
|
||||
const date = new Date(dt*1000)
|
||||
return date.toLocaleString(this.locale)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
locale() {
|
||||
const l = i18n.global.locale.value
|
||||
switch (l) {
|
||||
case "zhHans":
|
||||
return "zh-cn"
|
||||
case "zhHant":
|
||||
return "zh-tw"
|
||||
default:
|
||||
return l
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
this.changes = []
|
||||
this.user = this.$props.actor
|
||||
this.key = ''
|
||||
this.chngCount = 10
|
||||
if (newValue) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg" :loading="loading">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.client') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-skeleton-loader
|
||||
class="mx-auto border"
|
||||
width="95%"
|
||||
type="card, text, divider, list-item-two-line"
|
||||
v-if="loading"
|
||||
></v-skeleton-loader>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;" :hidden="loading">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('client.config') }}</v-tab>
|
||||
<v-tab value="t3">{{ $t('client.links') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="t1">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-combobox v-model="client.group" :items="groups" :label="$t('client.group')" hide-details></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="client.name" :label="$t('client.name')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="client.desc" :label="$t('client.desc')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="Volume" type="number" min="0" :label="$t('stats.volume')" suffix="GiB" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="!(client.delayStart && !client.autoReset)">
|
||||
<DatePick :expiry="expDate" @submit="setDate" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="client.autoReset || client.delayStart">
|
||||
<v-text-field v-model.number="resetDays" type="number" min="1" :label="$t('client.resetDays')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary"
|
||||
:disabled="client.up+client.down>0"
|
||||
v-model="delayStart"
|
||||
:label="$t('client.delayStart')" hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="autoReset" :label="$t('client.autoReset')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="id > 0">
|
||||
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
{{ $t('stats.usage') }}: {{ total }}<sup dir="ltr" v-if="percent>0">({{ percent }}%)</sup>
|
||||
</div>
|
||||
<v-btn density="compact" variant="text" icon="mdi-restore" @click="resetUsage">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('reset') }}
|
||||
</v-tooltip>
|
||||
<v-icon />
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
v-model="percent"
|
||||
:color="percentColor"
|
||||
v-if="client.volume>0"
|
||||
bottom
|
||||
>
|
||||
</v-progress-linear>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-icon icon="mdi-upload" color="orange" /><span class="text-orange">{{ up }}</span>
|
||||
/
|
||||
<v-icon icon="mdi-download" color="success" /><span class="text-success">{{ down }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="id >0 && client.autoReset">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<div class="text-medium-emphasis">{{ $t('client.nextReset') }}</div>
|
||||
<div dir="ltr">{{ nextResetFormatted }}</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<div class="text-medium-emphasis">{{ $t('main.stats.totalUsage') }}</div>
|
||||
<div>
|
||||
<v-icon icon="mdi-upload" color="orange" /><span class="text-orange">{{ totalUp }}</span>
|
||||
/
|
||||
<v-icon icon="mdi-download" color="success" /><span class="text-success">{{ totalDown }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="clientInbounds"
|
||||
:items="inboundTags"
|
||||
:label="$t('client.inboundTags')"
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
hide-details>
|
||||
<template v-slot:append>
|
||||
<v-icon @click="setAllInbounds" icon="mdi-set-all" v-tooltip:top="$t('all')" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="t2">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-btn variant="tonal" @click="shuffle()">{{ $t('reset') + ' - ' + $t('all') }}<v-icon icon="mdi-refresh" /></v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="key in Object.keys(clientConfig)">
|
||||
<v-col cols="12" md="3" align="end" align-self="center">
|
||||
{{ key }}
|
||||
<v-icon @click="shuffle(key)" icon="mdi-refresh" v-tooltip:top="$t('reset')" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-if="clientConfig[key].password != undefined"
|
||||
label="Password"
|
||||
v-model="clientConfig[key].password"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="clientConfig[key].uuid != undefined"
|
||||
label="UUID"
|
||||
v-model="clientConfig[key].uuid"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="key == 'vless'"
|
||||
label="Flow"
|
||||
v-model="clientConfig[key].flow"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="key == 'hysteria'"
|
||||
label="Auth"
|
||||
v-model="clientConfig[key].auth_str"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="t3">
|
||||
<v-row v-for="(lnk, index) in links">
|
||||
<v-col cols="auto">{{ index + 1 }}</v-col>
|
||||
<v-col style="direction: ltr; overflow-y: hidden;">{{ lnk.uri }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="extLinks.push({ type: 'external', uri: ''})">{{ $t('actions.add') }} {{ $t('client.external') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(lnk, index) in extLinks">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dir="ltr"
|
||||
:label="$t('client.external') + ' ' + (index+1)"
|
||||
append-icon="mdi-delete"
|
||||
@click:append="extLinks.splice(index,1)"
|
||||
placeholder="<protocol>://<data>"
|
||||
v-model="lnk.uri" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="subLinks.push({ type: 'sub', uri: ''})">{{ $t('actions.add') }} {{ $t('client.sub') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(lnk, index) in subLinks">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dir="ltr"
|
||||
:label="$t('client.sub') + ' ' + (index+1)"
|
||||
append-icon="mdi-delete"
|
||||
@click:append="subLinks.splice(index,1)"
|
||||
placeholder="http[s]://<domain>[:]<port>/<path>"
|
||||
v-model="lnk.uri" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { createClient, randomConfigs, updateConfigs, Link, shuffleConfigs } from '@/types/clients'
|
||||
import DatePick from '@/components/DateTime.vue'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import Data from '@/store/modules/data'
|
||||
import { locale } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'id', 'inboundTags', 'groups'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
client: createClient(),
|
||||
title: "add",
|
||||
loading: false,
|
||||
tab: "t1",
|
||||
clientConfig: <any>[],
|
||||
links: <Link[]>[],
|
||||
extLinks: <Link[]>[],
|
||||
subLinks: <Link[]>[],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateData(id: number) {
|
||||
if (id > 0) {
|
||||
this.loading = true
|
||||
const newData = await Data().loadClients(id)
|
||||
this.client = createClient(newData)
|
||||
this.title = "edit"
|
||||
this.clientConfig = this.client.config
|
||||
this.loading = false
|
||||
}
|
||||
else {
|
||||
this.client = createClient()
|
||||
this.title = "add"
|
||||
this.clientConfig = randomConfigs('client')
|
||||
}
|
||||
this.links = this.client.links?.filter(l => l.type == 'local')?? []
|
||||
this.extLinks = this.client.links?.filter(l => l.type == 'external')?? []
|
||||
this.subLinks = this.client.links?.filter(l => l.type == 'sub')?? []
|
||||
this.tab = "t1"
|
||||
this.loading = false
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData(0) // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
// check duplicate name
|
||||
const isDuplicateName = Data().checkClientName(this.$props.id, this.client.name)
|
||||
if (isDuplicateName) return
|
||||
|
||||
// check if delayStart is true and autoReset is false, set expiry to 0
|
||||
if (this.client.delayStart && !this.client.autoReset) this.client.expiry = 0
|
||||
|
||||
// save data
|
||||
this.loading = true
|
||||
this.client.config = updateConfigs(this.clientConfig, this.client.name)
|
||||
this.client.links = [
|
||||
...this.extLinks.filter(l => l.uri != ''),
|
||||
...this.subLinks.filter(l => l.uri != '')]
|
||||
const success = await Data().save("clients", this.$props.id == 0 ? "new" : "edit", this.client)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
setDate(newDate:number){
|
||||
this.client.expiry = newDate
|
||||
},
|
||||
setAllInbounds(){
|
||||
this.client.inbounds = this.inboundTags.map((i:any) => i.value).sort()
|
||||
},
|
||||
shuffle(k?:string) {
|
||||
shuffleConfigs(this.clientConfig, k)
|
||||
},
|
||||
resetUsage(){
|
||||
this.client.totalUp = (this.client.totalUp ?? 0) + this.client.up
|
||||
this.client.totalDown = (this.client.totalDown ?? 0) + this.client.down
|
||||
this.client.up = 0
|
||||
this.client.down = 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clientInbounds: {
|
||||
get() { return this.client.inbounds.length>0 ? this.client.inbounds.sort() : [] },
|
||||
set(v:number[]) { this.client.inbounds = v.length == 0 ? [] : v.sort() }
|
||||
},
|
||||
expDate: {
|
||||
get() { return this.client.expiry},
|
||||
set(v:any) { this.client.expiry = v }
|
||||
},
|
||||
Volume: {
|
||||
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
|
||||
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
|
||||
},
|
||||
delayStart: {
|
||||
get() { return this.client.delayStart?? false },
|
||||
set(v:boolean) {
|
||||
this.client.delayStart = v
|
||||
this.client.resetDays = v ? 1 : 0
|
||||
if (v && !this.autoReset) this.client.expiry = 0
|
||||
}
|
||||
},
|
||||
autoReset: {
|
||||
get() { return this.client.autoReset?? false },
|
||||
set(v:boolean) {
|
||||
this.client.autoReset = v
|
||||
this.client.resetDays = v ? 1 : 0
|
||||
if (!v) this.client.nextReset = 0
|
||||
}
|
||||
},
|
||||
resetDays: {
|
||||
get() { return this.client.resetDays?? 1 },
|
||||
set(v:number|null) {
|
||||
if (!v) v = 1
|
||||
if (this.client.nextReset && this.client.nextReset > 0) {
|
||||
this.client.nextReset += (v-(this.client.resetDays?? 0))*24*60*60
|
||||
}
|
||||
this.client.resetDays = v
|
||||
}
|
||||
},
|
||||
up() :string { return HumanReadable.sizeFormat(this.client.up) },
|
||||
down() :string { return HumanReadable.sizeFormat(this.client.down) },
|
||||
total() :string { return HumanReadable.sizeFormat(this.client.down + this.client.up) },
|
||||
totalUp() :string { return HumanReadable.sizeFormat((this.client.totalUp ?? 0) + this.client.up) },
|
||||
totalDown() :string { return HumanReadable.sizeFormat((this.client.totalDown ?? 0) + this.client.down) },
|
||||
nextResetFormatted() :string {
|
||||
const ts = this.client.nextReset?? 0
|
||||
if (ts == 0) return '-'
|
||||
const date = new Date(ts*1000)
|
||||
return date.toLocaleString(locale)
|
||||
},
|
||||
percent() :number { return this.client.volume>0 ? Math.round((this.client.up + this.client.down) *100 / this.client.volume) : 0 },
|
||||
percentColor() :string { return (this.client.up+this.client.down) >= this.client.volume ? 'error' : this.percent>90 ? 'warning' : 'success' },
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData(this.$props.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { DatePick },
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.addbulk') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="count" type="number" min="1" max="100" :label="$t('count')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-combobox
|
||||
chips
|
||||
multiple
|
||||
v-model="bulkData.name"
|
||||
:items="patterns"
|
||||
:label="$t('client.name')"
|
||||
hide-details>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-combobox
|
||||
chips
|
||||
multiple
|
||||
v-model="bulkData.desc"
|
||||
:items="patterns"
|
||||
:label="$t('client.desc')"
|
||||
hide-details>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-combobox v-model="bulkData.group" :items="groups" :label="$t('client.group')" hide-details></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="bulkData.Volume" type="number" min="0" :label="$t('stats.volume')" suffix="GiB" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="!(bulkData.delayStart && !bulkData.autoReset)">
|
||||
<DatePick :expiry="bulkData.expiry" @submit="setDate" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary"
|
||||
v-model="bulkData.delayStart"
|
||||
:label="$t('client.delayStart')" hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="bulkData.autoReset" :label="$t('client.autoReset')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="bulkData.autoReset || bulkData.delayStart">
|
||||
<v-text-field v-model.number="bulkData.resetDays" type="number" min="1" :label="$t('client.resetDays')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="bulkData.clientInbounds"
|
||||
:items="inboundTags"
|
||||
:label="$t('client.inboundTags')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-icon @click="setAllInbounds" icon="mdi-set-all" v-tooltip:top="$t('all')" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DatePick from '@/components/DateTime.vue'
|
||||
import { push } from 'notivue'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import { Client, createClient, randomConfigs } from '@/types/clients'
|
||||
import { i18n } from '@/locales'
|
||||
import Data from '@/store/modules/data'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'inboundTags', 'groups'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
count: 1,
|
||||
clients: <Client[]>[],
|
||||
bulkData: {
|
||||
name: <any[]>[],
|
||||
desc: <any[]>[],
|
||||
group: '',
|
||||
clientInbounds: [],
|
||||
expiry: 0,
|
||||
Volume: 0,
|
||||
delayStart: false,
|
||||
autoReset: false,
|
||||
resetDays: 0,
|
||||
},
|
||||
patterns: [
|
||||
{ title: i18n.global.t("bulk.random"), value: "random" },
|
||||
{ title: i18n.global.t("bulk.order"), value: "order" },
|
||||
],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetData() {
|
||||
this.count = 1,
|
||||
this.clients = [],
|
||||
this.bulkData = {
|
||||
name: [this.patterns[1], "-", this.patterns[0]],
|
||||
desc: [],
|
||||
group: '',
|
||||
clientInbounds: [],
|
||||
expiry: 0,
|
||||
Volume: 0,
|
||||
delayStart: false,
|
||||
autoReset: false,
|
||||
resetDays: 0,
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
if (this.bulkData.name.findIndex(n => typeof(n) == 'object') == -1) {
|
||||
push.error(i18n.global.t('error.dplData'))
|
||||
return
|
||||
}
|
||||
this.clients = []
|
||||
this.loading = true
|
||||
for(let i=0;i<this.count;i++){
|
||||
const name = this.genByPattern(this.bulkData.name, i)
|
||||
this.clients.push(createClient({
|
||||
enable: true,
|
||||
name: name,
|
||||
config: randomConfigs(name),
|
||||
inbounds: this.bulkData.clientInbounds.length > 0 ? this.bulkData.clientInbounds.sort() : [],
|
||||
links: [],
|
||||
volume: this.bulkData.Volume*(1024 ** 3),
|
||||
expiry: (this.bulkData.delayStart && !this.bulkData.autoReset) ? 0 : this.bulkData.expiry,
|
||||
up: 0,
|
||||
down: 0,
|
||||
desc: this.genByPattern(this.bulkData.desc, i),
|
||||
group: this.bulkData.group,
|
||||
delayStart: this.bulkData.delayStart,
|
||||
autoReset: this.bulkData.autoReset,
|
||||
resetDays: this.bulkData.resetDays,
|
||||
}))
|
||||
}
|
||||
// Check duplicate names
|
||||
const isDuplicateName = Data().checkBulkClientNames(this.clients.map(c => c.name))
|
||||
if (isDuplicateName) return
|
||||
const success = await Data().save("clients", "addbulk", this.clients)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
genByPattern(pattern: any[], order :number){
|
||||
if (pattern.length == 0) return RandomUtil.randomSeq(8)
|
||||
let result = ''
|
||||
pattern.forEach(p => {
|
||||
switch(typeof p){
|
||||
case 'object':
|
||||
switch(p.value){
|
||||
case "random":
|
||||
result += RandomUtil.randomSeq(8)
|
||||
break
|
||||
case "order":
|
||||
result += order+1
|
||||
}
|
||||
break
|
||||
default:
|
||||
result += p
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
setDate(v:number){
|
||||
this.bulkData.expiry = v
|
||||
},
|
||||
setAllInbounds(){
|
||||
this.bulkData.clientInbounds = this.inboundTags.map((i:any) => i.value).sort()
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.resetData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { DatePick },
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.editbulk') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-card :subtitle="$t('actions.action')" class="mb-4">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="actionMode"
|
||||
:items="actionModes"
|
||||
:label="$t('actions.action')"
|
||||
hide-details
|
||||
@update:model-value="onActionChange"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="actionMode === 'change_limits'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model.number="editData.addDays"
|
||||
type="number"
|
||||
:label="$t('bulk.addDays')"
|
||||
:suffix="$t('date.d')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model.number="editData.addVolume"
|
||||
type="number"
|
||||
:label="$t('bulk.addVolume')"
|
||||
:suffix="$t('stats.GB')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch
|
||||
v-model="editData.enable"
|
||||
:label="$t('enable')"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="actionMode === 'add_inbounds' || actionMode === 'remove_inbounds'">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-select
|
||||
v-model="editData.inboundTags"
|
||||
:items="inboundTags"
|
||||
:label="$t('client.inboundTags')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Section two: Clients (like init users in Inbound modal) -->
|
||||
<Users :clients="clients" :data="selectedClients" />
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
:disabled="selectedClients.values.length == 0"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Users from '@/components/Users.vue'
|
||||
import { i18n } from '@/locales'
|
||||
import Data from '@/store/modules/data';
|
||||
import { Client } from '@/types/clients';
|
||||
|
||||
export default {
|
||||
props: ['visible', 'clients', 'inboundTags'],
|
||||
emits: ['close'],
|
||||
components: { Users },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
actionMode: 'change_limits',
|
||||
actionModes: [
|
||||
{ title: i18n.global.t('bulk.changeLimits'), value: 'change_limits' },
|
||||
{ title: i18n.global.t('bulk.addInbounds'), value: 'add_inbounds' },
|
||||
{ title: i18n.global.t('bulk.removeInbounds'), value: 'remove_inbounds' },
|
||||
{ title: i18n.global.t('actions.delbulk'), value: 'delete_bulk' },
|
||||
],
|
||||
editData: {
|
||||
enable: true,
|
||||
addDays: 0,
|
||||
addVolume: 0,
|
||||
inboundTags: [] as number[],
|
||||
},
|
||||
selectedClients: {
|
||||
model: 'none',
|
||||
values: [] as any[],
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onActionChange() {
|
||||
this.editData.inboundTags = []
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
getTargetClients(): Client[] {
|
||||
const clients = this.clients ?? []
|
||||
switch (this.selectedClients.model) {
|
||||
case 'all':
|
||||
return clients
|
||||
case 'group':
|
||||
return clients
|
||||
.filter((c: any) => this.selectedClients.values.includes(c.group))
|
||||
case 'client':
|
||||
return clients.filter((c: any) => this.selectedClients.values.includes(c.id))
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
async saveChanges() {
|
||||
this.loading = true
|
||||
const targetClients = this.getTargetClients()
|
||||
switch (this.actionMode) {
|
||||
case 'change_limits':
|
||||
targetClients.forEach((c: Client) => {
|
||||
if (this.editData.addVolume != 0 && c.volume > 0)
|
||||
c.volume += this.editData.addVolume*(1024 ** 3)
|
||||
if (this.editData.addDays != 0 && c.expiry > 0)
|
||||
c.expiry += this.editData.addDays*(24 * 60 * 60)
|
||||
if (this.editData.enable)
|
||||
c.enable = (c.volume == 0 || c.up + c.down < c.volume) && (c.expiry == 0 || c.expiry > Date.now()/1000)
|
||||
})
|
||||
break
|
||||
case 'add_inbounds':
|
||||
targetClients.forEach((c: Client) => {
|
||||
this.editData.inboundTags.forEach((t: number) => {
|
||||
if (!c.inbounds.includes(t)) {
|
||||
c.inbounds.push(t)
|
||||
}
|
||||
})
|
||||
c.inbounds = c.inbounds.sort()
|
||||
})
|
||||
break
|
||||
case 'remove_inbounds':
|
||||
targetClients.forEach((c: Client) => {
|
||||
c.inbounds = c.inbounds.filter((i: number) => !this.editData.inboundTags.includes(i))
|
||||
})
|
||||
break
|
||||
case 'delete_bulk':
|
||||
const success = await Data().save("clients", "delbulk", targetClients.map((c: Client) => c.id))
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
const success = await Data().save("clients", 'editbulk', targetClients)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal) {
|
||||
this.actionMode = 'change_limits'
|
||||
this.editData = { enable: true, addDays: 0, addVolume: 0, inboundTags: [] }
|
||||
this.selectedClients = { model: 'none', values: [] }
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.dnsserver') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="dnsServer.type"
|
||||
:items="dnsTypes"
|
||||
:label="$t('type')"
|
||||
@update:modelValue="changeType"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dnsServer.tag" :label="$t('objects.tag')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="HasServer.includes(dnsServer.type)">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dnsServer.server" :label="$t('in.addr')" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="dnsServer.server_port" type="number" min="0" :label="$t('in.port')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="HasHeaders.includes(dnsServer.type)">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="dnsServer.path" :label="$t('transport.path')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<DialVue :dial="dnsServer" v-if="!WithoutDial.includes(dnsServer.type)" />
|
||||
<oTlsVue :outbound="dnsServer" v-if="HasTls.includes(dnsServer.type)" />
|
||||
<Headers :data="dnsServer" v-if="HasHeaders.includes(dnsServer.type)" />
|
||||
<template v-if="dnsServer.type == 'hosts'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="hostsPath" :label="$t('transport.path') + $t('commaSeparated')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card>
|
||||
<v-card-subtitle>Predefined
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="addHostsPredefined"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-card-subtitle>
|
||||
<v-row v-for="(pd, index) in hostsPredefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="pd.name" :label="$t('setting.domain')" @input="update_pds_key(index,$event.target.value)" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="pd.value"
|
||||
:label="$t('types.tun.addr') + $t('commaSeparated')"
|
||||
@input="update_pds_value(index,$event.target.value)"
|
||||
hide-details>
|
||||
<template v-slot:append>
|
||||
<v-icon @click="delHostsPredefined(index)" color="error" icon="mdi-delete" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
<v-row v-if="dnsServer.type == 'local'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dnsServer.prefer_go" color="primary" :label="$t('dns.local.preferGo')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="dnsServer.type == 'dhcp'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dnsServer.interface" :label="$t('types.tun.ifName')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="dnsServer.type == 'fakeip'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dnsServer.inet4_range" :label="$t('dns.rule.inet4Range')" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="dnsServer.inet6_range" :label="$t('dns.rule.inet6Range')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="dnsServer.type == 'tailscale' || dnsServer.type == 'resolved'">
|
||||
<v-col cols="12" sm="6" md="4" v-if="dnsServer.type == 'tailscale'">
|
||||
<v-select v-model="dnsServer.endpoint" :label="$t('objects.endpoint')" :items="tsTags" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="dnsServer.type == 'resolved'">
|
||||
<v-select v-model="dnsServer.service" :label="$t('objects.service')" :items="rslvdTags" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dnsServer.accept_default_resolvers" :label="$t('dns.rule.acceptDefault')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue-darken-1" variant="outlined" @click="close">{{ $t('actions.close') }}</v-btn>
|
||||
<v-btn color="blue-darken-1" variant="tonal" @click="save">{{ $t('actions.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DialVue from '@/components/Dial.vue'
|
||||
import oTlsVue from '@/components/tls/OutTLS.vue'
|
||||
import Headers from '@/components/Headers.vue'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import { DnsTypes, createDnsServer } from '@/types/dns'
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'tsTags', 'rslvdTags'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
title: "add",
|
||||
dnsServer: createDnsServer("local",{tag: "dns-" + RandomUtil.randomSeq(3)}),
|
||||
dnsTypes: Object.keys(DnsTypes).map((key,index) => ({title: key, value: Object.values(DnsTypes)[index]})),
|
||||
HasServer: [DnsTypes.TCP, DnsTypes.UDP, DnsTypes.TLS, DnsTypes.QUIC, DnsTypes.HTTPS, DnsTypes.HTTP3],
|
||||
HasHeaders: [DnsTypes.HTTPS, DnsTypes.HTTP3],
|
||||
HasTls: [DnsTypes.TLS, DnsTypes.QUIC, DnsTypes.HTTPS, DnsTypes.HTTP3],
|
||||
WithoutDial: [DnsTypes.Hosts, DnsTypes.Tailscale, DnsTypes.FakeIP, DnsTypes.Resolved],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
this.dnsServer = JSON.parse(this.$props.data)
|
||||
this.title = 'edit'
|
||||
}
|
||||
else {
|
||||
this.dnsServer = createDnsServer("local",{tag: "dns-" + RandomUtil.randomSeq(3)})
|
||||
this.title = 'add'
|
||||
}
|
||||
},
|
||||
changeType(dnsType: string) {
|
||||
this.dnsServer = createDnsServer(dnsType,{tag: this.dnsServer.tag})
|
||||
},
|
||||
close() {
|
||||
this.$emit('close')
|
||||
},
|
||||
save() {
|
||||
this.$emit('save', this.dnsServer)
|
||||
},
|
||||
addHostsPredefined() {
|
||||
const newPredefined = { name:'localhost', value: '127.0.0.1,::1' }
|
||||
this.hostsPredefined = [...this.hostsPredefined, newPredefined]
|
||||
},
|
||||
delHostsPredefined(i:number) {
|
||||
let pds = this.hostsPredefined
|
||||
pds.splice(i,1)
|
||||
this.hostsPredefined = pds
|
||||
},
|
||||
update_pds_key(i:number,k:string) {
|
||||
let pds = this.hostsPredefined
|
||||
pds[i].name = k
|
||||
this.hostsPredefined = pds
|
||||
},
|
||||
update_pds_value(i:number,v:string) {
|
||||
let pds = this.hostsPredefined
|
||||
pds[i].value = v
|
||||
this.hostsPredefined = pds
|
||||
},
|
||||
},
|
||||
computed:{
|
||||
hostsPath: {
|
||||
get() { return this.dnsServer.path },
|
||||
set(v: string) {
|
||||
this.dnsServer.path = v.length > 0 ? v.split(',').map((item: string) => item.trim()) : undefined
|
||||
}
|
||||
},
|
||||
hostsPredefined: {
|
||||
get() :any[] {
|
||||
let pds :any[] = []
|
||||
const h = this.dnsServer.predefined
|
||||
if (h) {
|
||||
Object.keys(h).forEach(key => {
|
||||
if (Array.isArray(h[key])){
|
||||
pds.push({ name: key, value: h[key].join(',') })
|
||||
} else {
|
||||
pds.push({ name: key, value: h[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
return pds
|
||||
},
|
||||
set(v: any[]) {
|
||||
if (v.length>0) {
|
||||
let pds:any = {}
|
||||
v.forEach((pd:any) => {
|
||||
pds[pd.name] = pd.value.split(',').map((item: string) => item.trim())
|
||||
})
|
||||
this.dnsServer.predefined = pds
|
||||
} else {
|
||||
this.dnsServer.predefined = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { DialVue, oTlsVue, Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.dnsrule') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="logical" :label="$t('rule.logical')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto" v-if="logical" justify="center" align="center">
|
||||
<v-btn color="primary" @click="ruleData.rules.push(<dnsRule>{})" hide-details>{{ $t('actions.add') + " " + $t('objects.rule') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card style="background-color: inherit; margin-bottom: 5px;" v-for="(r, index) in ruleData.rules" v-if="ruleData.type == 'logical'">
|
||||
<v-card-subtitle>{{ $t('objects.rule') + ' ' + (Number(index)+1) }}
|
||||
<v-icon @click="ruleData.rules.splice(index,1)" icon="mdi-delete" v-if="ruleData.rules.length>1" />
|
||||
</v-card-subtitle>
|
||||
<v-card-text style="padding: 0;">
|
||||
<RuleOptions
|
||||
:rule="r"
|
||||
:clients="clients"
|
||||
:inTags="inTags"
|
||||
:ruleSets="ruleSets" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<RuleOptions
|
||||
v-else
|
||||
:rule="ruleData.rules[0]"
|
||||
:clients="clients"
|
||||
:inTags="inTags"
|
||||
:ruleSets="ruleSets" />
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.action"
|
||||
:items="actions"
|
||||
:label="$t('dns.rule.action.title')"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="logical">
|
||||
<v-combobox
|
||||
v-model="ruleData.mode"
|
||||
:items="['and', 'or']"
|
||||
:label="$t('rule.mode')"
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="ruleData.invert" :label="$t('rule.invert')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card :subtitle="$t('dns.rule.action.route')" v-if="['route', 'route-options'].includes(ruleData.action)">
|
||||
<v-row v-if="ruleData.action == 'route'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.server"
|
||||
:items="serverTags"
|
||||
:label="$t('dns.server')"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.strategy"
|
||||
:items="strategies"
|
||||
:label="$t('rule.strategy')"
|
||||
clearable
|
||||
@click:clear="delete ruleData.strategy"
|
||||
hide-details>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="ruleData.disable_cache" :label="$t('dns.disableCache')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="ruleData.rewrite_ttl" type="number" min="0" :label="$t('dns.rule.action.rewriteTtl')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="ruleData.client_subnet" :label="$t('dns.rule.action.clientSubnet')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card :subtitle="$t('dns.rule.action.reject')" v-if="ruleData.action == 'reject'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.method"
|
||||
:items="[{ title: 'Default', value: 'default' },{ title: 'Drop', value: 'drop'}]"
|
||||
:label="$t('rule.method')"
|
||||
clearable
|
||||
@click:clear="delete ruleData.method"
|
||||
hide-details>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="ruleData.no_drop" :label="$t('rule.noDrop')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card :subtitle="$t('dns.rule.action.predefined')" v-if="ruleData.action == 'predefined'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.rcode"
|
||||
:items="predefinedRcode"
|
||||
:label="$t('dns.rule.action.rcode')"
|
||||
clearable
|
||||
@click:clear="delete ruleData.rcode"
|
||||
hide-details>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="ruleData.rcode == 'NOERROR'">
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="answer" :label="$t('dns.rule.action.answer') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="ns" :label="$t('dns.rule.action.ns') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field v-model="extra" :label="$t('dns.rule.action.extra') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { logicalDnsRule, dnsRule, actionDnsRuleKeys } from '@/types/dns'
|
||||
import RuleOptions from '@/components/DnsRule.vue'
|
||||
import { i18n } from '@/locales'
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'clients', 'inTags', 'serverTags', 'ruleSets'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
title: 'add',
|
||||
loading: false,
|
||||
ruleData: <any>{
|
||||
type: 'logical',
|
||||
mode: 'and',
|
||||
rules: <dnsRule[]>[{}],
|
||||
invert: false,
|
||||
action: 'route',
|
||||
server: 'local',
|
||||
},
|
||||
actions: [
|
||||
{ title: i18n.global.t('dns.rule.action.route'), value: 'route'},
|
||||
{ title: i18n.global.t('dns.rule.action.routeOptions'), value: 'route-options'},
|
||||
{ title: i18n.global.t('dns.rule.action.reject'), value: 'reject'},
|
||||
{ title: i18n.global.t('dns.rule.action.predefined'), value: 'predefined'},
|
||||
],
|
||||
strategies: [
|
||||
{ title: 'Prefer IPv4', value: 'prefer_ipv4' },
|
||||
{ title: 'Prefer IPv6', value: 'prefer_ipv6' },
|
||||
{ title: 'IPv4 Only', value: 'ipv4_only' },
|
||||
{ title: 'IPv6 Only', value: 'ipv6_only' },
|
||||
],
|
||||
predefinedRcode: [
|
||||
{ title: i18n.global.t('dns.rule.action.rcodes.noError'), value: 'NOERROR' },
|
||||
{ title: i18n.global.t('dns.rule.action.rcodes.formerr'), value: 'FORMERR' },
|
||||
{ title: i18n.global.t('dns.rule.action.rcodes.servFail'), value: 'SERVFAIL' },
|
||||
{ title: i18n.global.t('dns.rule.action.rcodes.nxDomain'), value: 'NXDOMAIN' },
|
||||
{ title: i18n.global.t('dns.rule.action.rcodes.notImp'), value: 'NOTIMP' },
|
||||
{ title: i18n.global.t('dns.rule.action.rcodes.refused'), value: 'REFUSED' },
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
if (newData.type) {
|
||||
this.ruleData = newData
|
||||
} else {
|
||||
this.ruleData = {
|
||||
type: 'simple',
|
||||
mode: 'and',
|
||||
rules: <dnsRule[]>[{}],
|
||||
}
|
||||
Object.keys(newData).forEach(key => {
|
||||
if (actionDnsRuleKeys.includes(key)) {
|
||||
this.ruleData[key] = newData[key]
|
||||
} else {
|
||||
this.ruleData.rules[0][key] = newData[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
this.title = 'edit'
|
||||
}
|
||||
else {
|
||||
this.ruleData = <logicalDnsRule>{
|
||||
type: 'simple',
|
||||
mode: 'and',
|
||||
rules: <dnsRule[]>[{}],
|
||||
invert: false,
|
||||
action: 'route',
|
||||
server: this.$props.serverTags[0]?? 'local',
|
||||
}
|
||||
this.title = 'add'
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
let newRule = <any>{
|
||||
action: this.ruleData.action,
|
||||
invert: this.ruleData.invert? this.ruleData.invert : undefined,
|
||||
}
|
||||
|
||||
// Filter action data
|
||||
switch (newRule.action){
|
||||
case 'route':
|
||||
newRule.server = this.ruleData.server
|
||||
newRule.strategy = this.ruleData.strategy?.length > 0 ? this.ruleData.strategy : undefined
|
||||
newRule.disable_cache = this.ruleData.disable_cache? true : undefined
|
||||
newRule.rewrite_ttl = this.ruleData.rewrite_ttl > 0 ? this.ruleData.rewrite_ttl : undefined
|
||||
newRule.client_subnet = this.ruleData.client_subnet?.length > 0 ? this.ruleData.client_subnet : undefined
|
||||
break
|
||||
case 'route-options':
|
||||
newRule.disable_cache = this.ruleData.disable_cache? true : undefined
|
||||
newRule.rewrite_ttl = this.ruleData.rewrite_ttl > 0 ? this.ruleData.rewrite_ttl : undefined
|
||||
newRule.client_subnet = this.ruleData.client_subnet?.length > 0 ? this.ruleData.client_subnet : undefined
|
||||
break
|
||||
case 'reject':
|
||||
newRule.method = this.ruleData.method?.length > 0 ? this.ruleData.method : undefined
|
||||
newRule.no_drop = this.ruleData.no_drop? true : undefined
|
||||
break
|
||||
case 'predefined':
|
||||
newRule.rcode = this.ruleData.rcode?.length > 0 ? this.ruleData.rcode : undefined
|
||||
if (this.ruleData.rcode == 'NOERROR') {
|
||||
newRule.answer = this.ruleData.answer
|
||||
newRule.ns = this.ruleData.ns
|
||||
newRule.extra = this.ruleData.extra
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Add rules
|
||||
if (this.ruleData.type == 'simple'){
|
||||
newRule = { ...this.ruleData.rules[0], ...newRule }
|
||||
} else {
|
||||
newRule.type = 'logical'
|
||||
newRule.mode = this.ruleData.mode
|
||||
newRule.rules = this.ruleData.rules
|
||||
}
|
||||
this.$emit('save', newRule)
|
||||
this.loading = false
|
||||
},
|
||||
deleteRule(index:number) {
|
||||
this.ruleData.rules.splice(index,1)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
logical: {
|
||||
get() { return this.ruleData.type == 'logical' },
|
||||
set(v:boolean) {
|
||||
this.ruleData.type = v? 'logical' : 'simple'
|
||||
}
|
||||
},
|
||||
answer: {
|
||||
get() { return this.ruleData.answer?.length > 0 ? this.ruleData.answer.join(',') : "" },
|
||||
set(v:string) { this.ruleData.answer = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
ns: {
|
||||
get() { return this.ruleData.ns?.length > 0 ? this.ruleData.ns.join(',') : "" },
|
||||
set(v:string) { this.ruleData.ns = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
extra: {
|
||||
get() { return this.ruleData.extra?.length > 0 ? this.ruleData.extra.join(',') : "" },
|
||||
set(v:string) { this.ruleData.extra = v.length > 0 ? v.split(',') : undefined }
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { RuleOptions }
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.endpoint') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:disabled="endpoint.id > 0"
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(epTypes).map((key,index) => ({title: key, value: Object.values(epTypes)[index]}))"
|
||||
v-model="endpoint.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="endpoint.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Wireguard v-if="endpoint.type == epTypes.Wireguard"
|
||||
:data="endpoint"
|
||||
@getWgPubKey="getWgPubKey"
|
||||
@newWgKey="newWgKey"
|
||||
@addPeer="addWgPeer"
|
||||
@delPeer="delWgPeer"
|
||||
@refreshPeerKey="refreshWgPeerKey" />
|
||||
<Warp v-if="endpoint.type == epTypes.Warp" :data="endpoint" />
|
||||
<TailscaleVue v-if="endpoint.type == epTypes.Tailscale" :data="endpoint" />
|
||||
<Dial :dial="endpoint" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { EpTypes, createEndpoint } from '@/types/endpoints'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import Dial from '@/components/Dial.vue'
|
||||
import Wireguard from '@/components/protocols/Wireguard.vue'
|
||||
import Warp from '@/components/protocols/Warp.vue'
|
||||
import TailscaleVue from '@/components/protocols/Tailscale.vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
import Data from '@/store/modules/data'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'tags'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
endpoint: createEndpoint("wireguard",{ "tag": "" }),
|
||||
title: "add",
|
||||
tab: "t1",
|
||||
loading: false,
|
||||
epTypes: EpTypes,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateData(id: number) {
|
||||
if (id > 0) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.endpoint = createEndpoint(newData.type, newData)
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
this.endpoint.type = "wireguard"
|
||||
this.endpoint.listen_port = RandomUtil.randomIntRange(10000, 60000)
|
||||
this.changeType()
|
||||
this.title = "add"
|
||||
}
|
||||
this.tab = "t1"
|
||||
},
|
||||
async changeType() {
|
||||
// Tag change only in add endpoint
|
||||
const tag = this.endpoint.type + "-" + RandomUtil.randomSeq(3)
|
||||
|
||||
// Use previous data
|
||||
let prevConfig = {}
|
||||
switch (this.endpoint.type) {
|
||||
case EpTypes.Wireguard:
|
||||
const wgKeys = (await this.genWgKey())
|
||||
const randomIPoctet = RandomUtil.randomIntRange(1, 255)
|
||||
prevConfig = {
|
||||
tag: tag,
|
||||
listen_port: this.endpoint.listen_port ?? RandomUtil.randomIntRange(10000, 60000),
|
||||
address: ['10.0.0.'+ randomIPoctet.toString() +'/32','fe80::'+ randomIPoctet.toString(16) +'/128'],
|
||||
private_key: wgKeys.private_key,
|
||||
peers: [],
|
||||
ext: {
|
||||
public_key: wgKeys.public_key,
|
||||
keys: []
|
||||
}
|
||||
}
|
||||
break
|
||||
case EpTypes.Warp:
|
||||
prevConfig = {
|
||||
tag: tag,
|
||||
}
|
||||
break
|
||||
case EpTypes.Tailscale:
|
||||
prevConfig = { tag: tag }
|
||||
break
|
||||
}
|
||||
this.endpoint = createEndpoint(this.endpoint.type, prevConfig)
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData(0) // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
|
||||
// check duplicate tag
|
||||
const isDuplicatedTag = Data().checkTag("endpoint",this.endpoint.id, this.endpoint.tag)
|
||||
if (isDuplicatedTag) return
|
||||
|
||||
// save data
|
||||
this.loading = true
|
||||
const success = await Data().save("endpoints", this.$props.id == 0 ? "new" : "edit", this.endpoint)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
async genWgKey(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "wireguard" })
|
||||
this.loading = false
|
||||
let result = { private_key: "", public_key: "" }
|
||||
if (msg.success) {
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (line.startsWith("PrivateKey")){
|
||||
result.private_key = line.substring(12)
|
||||
}
|
||||
if (line.startsWith("PublicKey")){
|
||||
result.public_key = line.substring(11)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
return result
|
||||
},
|
||||
async newWgKey(){
|
||||
this.loading = true
|
||||
const newKeys = await this.genWgKey()
|
||||
this.endpoint.private_key = newKeys.private_key
|
||||
if (!this.endpoint.ext) this.endpoint.ext = {keys: []}
|
||||
this.endpoint.ext.public_key = newKeys.public_key
|
||||
this.loading = false
|
||||
},
|
||||
async getWgPubKey(private_key: string) {
|
||||
if (!this.endpoint.ext) this.endpoint.ext = {keys: []}
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "wireguard", o: private_key })
|
||||
if (msg.success) {
|
||||
this.endpoint.ext.public_key = msg.obj[0]
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
async addWgPeer(){
|
||||
if (this.endpoint.type != EpTypes.Wireguard) return
|
||||
this.loading = true
|
||||
const newKeys = await this.genWgKey()
|
||||
if (!this.endpoint.ext) this.endpoint.ext = {keys: []}
|
||||
this.endpoint.ext.keys.push(newKeys)
|
||||
this.endpoint.peers.push({
|
||||
public_key: newKeys.public_key,
|
||||
allowed_ips: [this.findFreeIP()]
|
||||
})
|
||||
this.loading = false
|
||||
},
|
||||
findFreeIP(): string{
|
||||
const peerAllowedIPs = this.endpoint.peers.map((peer: any) => peer.allowed_ips).flat()
|
||||
for (let i = 2; i < 255; i++) {
|
||||
const newIP = '10.0.1.'+ i.toString() +'/32'
|
||||
if (!peerAllowedIPs.includes(newIP)) return newIP
|
||||
}
|
||||
return '0.0.0.0/0'
|
||||
},
|
||||
delWgPeer(index: number){
|
||||
if (this.endpoint.type != EpTypes.Wireguard) return
|
||||
this.endpoint.ext.keys = this.endpoint.ext.keys.filter((key: any) => key.public_key != this.endpoint.peers[index].public_key)
|
||||
this.endpoint.peers.splice(index, 1)
|
||||
},
|
||||
async refreshWgPeerKey(index: number) {
|
||||
this.loading = true
|
||||
const newKeys = await this.genWgKey()
|
||||
if (!this.endpoint.ext) this.endpoint.ext = {keys: []}
|
||||
const indexKeys = this.endpoint.ext.keys.findIndex((key: any) => key.public_key == this.endpoint.peers[index].public_key)
|
||||
this.endpoint.ext.keys[indexKeys == -1 ? this.endpoint.ext.keys.length : indexKeys] = newKeys
|
||||
this.endpoint.peers[index].public_key = newKeys.public_key
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.updateData(this.$props.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Dial, Wireguard, Warp, TailscaleVue }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800" @after-enter="updateData(id)">
|
||||
<v-card class="rounded-lg" :loading="loading">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.inbound') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-skeleton-loader
|
||||
class="mx-auto border"
|
||||
width="95%"
|
||||
type="card, text, divider, list-item-two-line"
|
||||
v-if="loading"
|
||||
></v-skeleton-loader>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;" :hidden="loading">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
|
||||
v-model="inbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="inbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-tabs
|
||||
v-if="HasInData.includes(inbound.type)"
|
||||
v-model="side"
|
||||
density="compact"
|
||||
fixed-tabs
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="s">{{ $t('in.sSide') }}</v-tab>
|
||||
<v-tab value="c">{{ $t('in.cSide') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="side" style="margin-top: 10px;">
|
||||
<v-window-item value="s">
|
||||
<Listen :data="inbound" :inTags="inTags" v-if="inbound.type != inTypes.Tun" />
|
||||
<Direct v-if="inbound.type == inTypes.Direct" :data="inbound" />
|
||||
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" direction="in" :data="inbound" />
|
||||
<Hysteria v-if="inbound.type == inTypes.Hysteria" direction="in" :data="inbound" />
|
||||
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" direction="in" :data="inbound" />
|
||||
<Naive v-if="inbound.type == inTypes.Naive" direction="in" :data="inbound" />
|
||||
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" direction="in" :data="inbound" />
|
||||
<Tuic v-if="inbound.type == inTypes.TUIC" direction="in" :data="inbound" />
|
||||
<Tun v-if="inbound.type == inTypes.Tun" :data="inbound" />
|
||||
<AnyTls v-if="inbound.type == inTypes.AnyTls" :data="inbound" direction="in" />
|
||||
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
|
||||
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
|
||||
<Users v-if="hasUser" :clients="clients" :data="initUsers" />
|
||||
<InTls v-if="HasTls.includes(inbound.type)" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="inbound.tls_id" />
|
||||
<Multiplex v-if="MuxAvailable.includes(inbound.type)" direction="in" :data="inbound" />
|
||||
</v-window-item>
|
||||
<v-window-item value="c">
|
||||
<OutJsonVue :inData="inbound" :type="inbound.type" />
|
||||
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="out" :data="inbound.out_json" />
|
||||
<Dial v-if="inbound.out_json" :dial="inbound.out_json" mode="client" />
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-card-subtitle>{{ $t('in.multiDomain') }}
|
||||
<v-chip color="primary" density="compact" variant="elevated" @click="add_addr"><v-icon icon="mdi-plus" /></v-chip>
|
||||
</v-card-subtitle>
|
||||
<template v-for="addr,index in inbound.addrs">
|
||||
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" color="error" @click="inbound.addrs?.splice(index,1)" />
|
||||
<v-divider></v-divider>
|
||||
<AddrVue :addr="addr" :hasTls="HasTls.includes(inbound.type)" />
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes, createInbound, Addr, ShadowTLS } from '@/types/inbounds'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import Dial from '@/components/Dial.vue'
|
||||
import Listen from '@/components/Listen.vue'
|
||||
import Direct from '@/components/protocols/Direct.vue'
|
||||
import Users from '@/components/Users.vue'
|
||||
import Shadowsocks from '@/components/protocols/Shadowsocks.vue'
|
||||
import Hysteria from '@/components/protocols/Hysteria.vue'
|
||||
import Hysteria2 from '@/components/protocols/Hysteria2.vue'
|
||||
import Naive from '@/components/protocols/Naive.vue'
|
||||
import ShadowTls from '@/components/protocols/ShadowTls.vue'
|
||||
import Tuic from '@/components/protocols/Tuic.vue'
|
||||
import Tun from '@/components/protocols/Tun.vue'
|
||||
import AnyTls from '@/components/protocols/AnyTls.vue'
|
||||
import InTls from '@/components/tls/InTLS.vue'
|
||||
import TProxy from '@/components/protocols/TProxy.vue'
|
||||
import Multiplex from '@/components/Multiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
import AddrVue from '@/components/Addr.vue'
|
||||
import OutJsonVue from '@/components/OutJson.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
export default {
|
||||
props: ['visible', 'id', 'inTags', 'tlsConfigs'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
inbound: createInbound("direct",{ id:0, "tag": "" }),
|
||||
title: "add",
|
||||
loading: false,
|
||||
side: "s",
|
||||
inTypes: InTypes,
|
||||
inboundWithUsers: ['mixed', 'socks', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless', 'anytls'],
|
||||
initUsers: {
|
||||
model: 'none',
|
||||
values: <any>[],
|
||||
},
|
||||
HasInData: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.HTTP,
|
||||
InTypes.Mixed,
|
||||
InTypes.Shadowsocks,
|
||||
InTypes.VMess,
|
||||
InTypes.ShadowTLS,
|
||||
InTypes.Trojan,
|
||||
InTypes.Hysteria,
|
||||
InTypes.VLESS,
|
||||
InTypes.AnyTls,
|
||||
InTypes.TUIC,
|
||||
InTypes.Hysteria2,
|
||||
InTypes.Naive,
|
||||
],
|
||||
HasTls: [
|
||||
InTypes.HTTP,
|
||||
InTypes.VMess,
|
||||
InTypes.Trojan,
|
||||
InTypes.Naive,
|
||||
InTypes.Hysteria,
|
||||
InTypes.TUIC,
|
||||
InTypes.Hysteria2,
|
||||
InTypes.VLESS,
|
||||
InTypes.AnyTls,
|
||||
],
|
||||
MuxAvailable: [
|
||||
InTypes.VLESS,
|
||||
InTypes.VMess,
|
||||
InTypes.Trojan,
|
||||
InTypes.Shadowsocks,
|
||||
],
|
||||
OnlyTLS: [InTypes.Hysteria, InTypes.Hysteria2, InTypes.TUIC, InTypes.Naive, InTypes.AnyTls ],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData(id: number) {
|
||||
this.loading = true
|
||||
const inboundArray = await Data().loadInbounds([id])
|
||||
this.inbound = inboundArray[0]
|
||||
if (this.HasInData.includes(this.inbound.type) && this.inbound.out_json == null) {
|
||||
this.inbound.out_json = {}
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
updateData(id: number) {
|
||||
if (id > 0) {
|
||||
this.loadData(id)
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
const port = RandomUtil.randomIntRange(10000, 60000)
|
||||
this.inbound = createInbound("direct",{ id: 0, tag: "direct-"+port ,listen: "::", listen_port: port })
|
||||
if (this.HasInData.includes(this.inbound.type)){
|
||||
this.inbound.addrs = []
|
||||
this.inbound.out_json = {}
|
||||
} else {
|
||||
delete this.inbound.addrs
|
||||
delete this.inbound.out_json
|
||||
}
|
||||
this.title = "add"
|
||||
this.loading = false
|
||||
}
|
||||
this.side = "s"
|
||||
this.initUsers = {
|
||||
model: 'none',
|
||||
values: [],
|
||||
}
|
||||
},
|
||||
changeType() {
|
||||
if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000)
|
||||
// Tag change only in add inbound
|
||||
const tag = this.$props.id > 0 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
|
||||
// Use previous data
|
||||
const prevConfig = { id: this.inbound.id, tag: tag, listen: this.inbound.listen?? "::", listen_port: this.inbound.listen_port }
|
||||
this.inbound = createInbound(this.inbound.type, this.inbound.type != this.inTypes.Tun ? prevConfig : { tag: tag })
|
||||
if (this.HasInData.includes(this.inbound.type)){
|
||||
this.inbound.addrs = []
|
||||
this.inbound.out_json = {}
|
||||
} else {
|
||||
delete this.inbound.addrs
|
||||
delete this.inbound.out_json
|
||||
}
|
||||
this.side = "s"
|
||||
},
|
||||
add_addr() {
|
||||
this.inbound.addrs?.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData(0) // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
// check duplicate tag
|
||||
const isDuplicatedTag = Data().checkTag("inbound", this.inbound.id, this.inbound.tag)
|
||||
if (isDuplicatedTag) return
|
||||
|
||||
// save data
|
||||
this.loading = true
|
||||
let clientIds = []
|
||||
if (this.hasUser) {
|
||||
switch (this.initUsers.model) {
|
||||
case 'all':
|
||||
clientIds = this.clients.map((c:any) => c.id)
|
||||
break
|
||||
case 'group':
|
||||
clientIds = this.clients.filter((c:any) => this.initUsers.values.includes(c.group)).map((c:any) => c.id)
|
||||
break
|
||||
case 'client':
|
||||
clientIds = this.initUsers.values
|
||||
}
|
||||
}
|
||||
const success = await Data().save("inbounds", this.$props.id == 0 ? "new" : "edit", this.inbound, clientIds)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
validate() {
|
||||
if (this.inbound == undefined) return false
|
||||
if (this.inbound.tag == "") return false
|
||||
if (this.inbound.listen_port > 65535 || this.inbound.listen_port < 1) return false
|
||||
if (this.OnlyTLS.includes(this.inbound.type) && this.inbound.tls_id == 0) return false
|
||||
return true
|
||||
},
|
||||
clients() {
|
||||
return Data().clients?? []
|
||||
},
|
||||
hasUser() {
|
||||
if (this.$props.id > 0) return false
|
||||
if (!this.inboundWithUsers.includes(this.inbound.type)) return false
|
||||
if (this.inbound.type == InTypes.ShadowTLS && (<ShadowTLS>this.inbound).version < 3 ) return false
|
||||
if ((<any>this.inbound).managed) return false
|
||||
return true
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.loading = true
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks,
|
||||
Users, Hysteria, ShadowTls, TProxy, Multiplex, Tuic, Tun,
|
||||
AnyTls, Transport, AddrVue, OutJsonVue, Dial
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="1200" :loading="loading">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('basic.log.title') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-icon icon="mdi-close" @click="control.visible = false" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.log.level')"
|
||||
:items="logLevels"
|
||||
v-model="logLevel"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('count')"
|
||||
:items="[10,20,30,50,100]"
|
||||
v-model.number="logCount"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="auto" align="center" justify="center">
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="loadData">
|
||||
<v-icon />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card style="background-color: background" dir="ltr" v-html="lines.join('<br />')"></v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
|
||||
export default {
|
||||
props: ['control', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
lines: [],
|
||||
logLevel: 'info',
|
||||
logLevels: [
|
||||
{ title: 'DEBUG', value: 'debug' },
|
||||
{ title: 'INFO', value: 'info' },
|
||||
{ title: 'WARNING', value: 'warning' },
|
||||
{ title: 'ERROR', value: 'err' },
|
||||
],
|
||||
logCount: 10,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/logs',{ c: this.logCount, l: this.logLevel })
|
||||
if (data.success) {
|
||||
this.lines = data.obj?? []
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
this.lines = []
|
||||
this.logLevel = 'info'
|
||||
this.logCount = 10
|
||||
if (v) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.outbound') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('client.external') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="t1">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(outTypes).map((key,index) => ({title: key, value: Object.values(outTypes)[index]}))"
|
||||
v-model="outbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="outbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!NoServer.includes(outbound.type)">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
v-model="outbound.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model.number="outbound.server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Socks v-if="outbound.type == outTypes.SOCKS" :data="outbound" />
|
||||
<Http v-if="outbound.type == outTypes.HTTP" :data="outbound" />
|
||||
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
|
||||
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
|
||||
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
|
||||
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
|
||||
<Naive v-if="outbound.type == outTypes.Naive" direction="out" :data="outbound" />
|
||||
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
|
||||
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
|
||||
<Tuic v-if="outbound.type == outTypes.TUIC" direction="out" :data="outbound" />
|
||||
<Hysteria2 v-if="outbound.type == outTypes.Hysteria2" direction="out" :data="outbound" />
|
||||
<AnyTls v-if="outbound.type == outTypes.AnyTls" :data="outbound" direction="out" />
|
||||
<Tor v-if="outbound.type == outTypes.Tor" :data="outbound" />
|
||||
<Ssh v-if="outbound.type == outTypes.SSH" :data="outbound" />
|
||||
<Selector v-if="outbound.type == outTypes.Selector" :data="outbound" :tags="tags" />
|
||||
<UrlTest v-if="outbound.type == outTypes.URLTest" :data="outbound" :tags="tags" />
|
||||
|
||||
<Transport v-if="Object.hasOwn(outbound,'transport')" :data="outbound" />
|
||||
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
|
||||
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
|
||||
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" />
|
||||
</v-window-item>
|
||||
<v-window-item value="t2">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field v-model="link" :label="$t('client.external')" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" align="center">
|
||||
<v-btn hide-details variant="tonal" :loading="loading" @click="linkConvert">{{ $t('submit') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { OutTypes, createOutbound } from '@/types/outbounds'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import Dial from '@/components/Dial.vue'
|
||||
import Multiplex from '@/components/Multiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
import OutTLS from '@/components/tls/OutTLS.vue'
|
||||
import Direct from '@/components/protocols/Direct.vue'
|
||||
import Socks from '@/components/protocols/Socks.vue'
|
||||
import Http from '@/components/protocols/Http.vue'
|
||||
import Shadowsocks from '@/components/protocols/Shadowsocks.vue'
|
||||
import Vmess from '@/components/protocols/Vmess.vue'
|
||||
import Trojan from '@/components/protocols/Trojan.vue'
|
||||
import Wireguard from '@/components/protocols/Wireguard.vue'
|
||||
import Hysteria from '@/components/protocols/Hysteria.vue'
|
||||
import Naive from '@/components/protocols/Naive.vue'
|
||||
import ShadowTls from '@/components/protocols/OutShadowTls.vue'
|
||||
import Vless from '@/components/protocols/Vless.vue'
|
||||
import Tuic from '@/components/protocols/Tuic.vue'
|
||||
import Hysteria2 from '@/components/protocols/Hysteria2.vue'
|
||||
import Tor from '@/components/protocols/Tor.vue'
|
||||
import Ssh from '@/components/protocols/Ssh.vue'
|
||||
import Selector from '@/components/protocols/Selector.vue'
|
||||
import UrlTest from '@/components/protocols/UrlTest.vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import AnyTls from '@/components/protocols/AnyTls.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'tags'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
outbound: createOutbound("direct",{ "tag": "" }),
|
||||
title: "add",
|
||||
tab: "t1",
|
||||
link: "",
|
||||
loading: false,
|
||||
outTypes: OutTypes,
|
||||
NoDial: [OutTypes.Selector, OutTypes.URLTest],
|
||||
NoServer: [OutTypes.Direct, OutTypes.Selector, OutTypes.URLTest, OutTypes.Tor],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData(id: number) {
|
||||
if (id > 0) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.outbound = createOutbound(newData.type, newData)
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
this.outbound = createOutbound("direct",{ tag: "direct-" + RandomUtil.randomSeq(3) })
|
||||
this.title = "add"
|
||||
}
|
||||
this.tab = "t1"
|
||||
},
|
||||
changeType() {
|
||||
// Tag change only in add outbound
|
||||
const tag = this.$props.id > 0 ? this.outbound.tag : this.outbound.type + "-" + RandomUtil.randomSeq(3)
|
||||
// Use previous data
|
||||
const prevConfig = { id: this.outbound.id, tag: tag, listen: this.outbound.listen, listen_port: this.outbound.listen_port }
|
||||
this.outbound = createOutbound(this.outbound.type, prevConfig)
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData(0) // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
// check duplicate tag
|
||||
const isDuplicatedTag = Data().checkTag("outbound",this.$props.id, this.outbound.tag)
|
||||
if (isDuplicatedTag) return
|
||||
|
||||
// save data
|
||||
this.loading = true
|
||||
const success = await Data().save("outbounds", this.$props.id == 0 ? "new" : "edit", this.outbound)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
async linkConvert() {
|
||||
if (this.link.length>0){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.post('api/linkConvert', { link: this.link })
|
||||
this.loading = false
|
||||
if (msg.success) {
|
||||
this.outbound = msg.obj
|
||||
if (this.$props.id > 0) this.outbound.id = this.$props.id
|
||||
this.tab = "t1"
|
||||
this.link = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData(this.$props.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Dial, Multiplex, Transport, OutTLS,
|
||||
Direct, Socks, Http, Shadowsocks, Vmess, Trojan,
|
||||
Wireguard, Hysteria, Naive, ShadowTls, Vless, Tuic,
|
||||
Hysteria2, AnyTls, Tor, Ssh, Selector, UrlTest }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800" :model-value="visible">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.addbulk') }} {{ $t('objects.outbound') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-row v-if="outbounds.length==0">
|
||||
<v-col cols="12">
|
||||
<v-text-field v-model="link"
|
||||
dir="ltr"
|
||||
:label="$t('client.sub')"
|
||||
placeholder="http[s]://<domain>[:]<port>/<path>"
|
||||
hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-checkbox v-model="addUrlTest" :label="$t('out.addUrlTest')" />
|
||||
</v-col>
|
||||
<v-col cols="12" align="center">
|
||||
<v-btn hide-details variant="tonal" :loading="loading" @click="linkCheck">{{ $t('submit') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-data-table
|
||||
v-if="outbounds.length>0"
|
||||
:items="outbounds"
|
||||
:loading="loading"
|
||||
:items-per-page="0"
|
||||
hide-default-footer
|
||||
density="compact"
|
||||
:headers="[
|
||||
{ value: 'check' },
|
||||
{ title: $t('type'), value: 'type' },
|
||||
{ title: $t('objects.tag'), value: 'tag' },
|
||||
{ title: $t('out.addr'), value: 'server' },
|
||||
{ title: $t('objects.tls'), value: 'tls' }
|
||||
]"
|
||||
>
|
||||
<template v-slot:[`item.check`]="{ index }">
|
||||
<v-icon color="success" icon="mdi-check" v-if="outChecks[index]==1" />
|
||||
<v-icon color="error" icon="mdi-close" v-else-if="outChecks[index]==2" />
|
||||
<v-progress-circular v-else-if="outChecks[index]==3" indeterminate />
|
||||
<v-icon v-else icon="mdi-help"></v-icon>
|
||||
</template>
|
||||
<template v-slot:[`item.type`]="{ item }">
|
||||
{{ item.type }}
|
||||
</template>
|
||||
<template v-slot:[`item.tag`]="{ item }">
|
||||
{{ item.tag }}
|
||||
</template>
|
||||
<template v-slot:[`item.tls`]="{ item }">
|
||||
{{ Object.hasOwn(item,'tls') ? $t(item.tls?.enabled ? 'enable' : 'disable') : '-' }}
|
||||
</template>
|
||||
<template v-slot:[`item.server`]="{ item }">
|
||||
{{ item.server }}{{ item.server_port ? ':' + item.server_port : '' }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="outlined" @click="closeModal">{{ $t('actions.close') }}</v-btn>
|
||||
<v-btn color="primary" variant="tonal" :loading="loading" :disabled="outbounds.length==0" @click="saveChanges">{{ $t('actions.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import RandomUtil from '@/plugins/randomUtil';
|
||||
import Data from '@/store/modules/data'
|
||||
import { createOutbound, Outbound } from '@/types/outbounds'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'outboundTags'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
link: "",
|
||||
outbounds: <Outbound[]>[],
|
||||
outChecks: <number[]>[],
|
||||
addUrlTest: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetData() {
|
||||
this.outbounds = []
|
||||
this.outChecks = []
|
||||
this.link = ""
|
||||
this.addUrlTest = false
|
||||
this.loading = false
|
||||
},
|
||||
closeModal() {
|
||||
this.resetData()
|
||||
this.$emit('close')
|
||||
},
|
||||
async linkCheck() {
|
||||
this.loading = true
|
||||
this.outbounds = []
|
||||
const msg = await HttpUtils.post('api/subConvert', { link: this.link })
|
||||
if (msg.success) {
|
||||
if (msg.obj?.length>0) {
|
||||
msg.obj.forEach((o:any, index:number) => {
|
||||
if (this.newOutboundTags.includes(o.tag)) o.tag = o.tag + "-" + (index+1)
|
||||
this.outbounds.push(createOutbound(o.type, o))
|
||||
this.outChecks.push(0)
|
||||
})
|
||||
if (this.addUrlTest) {
|
||||
const urlTestTsg = "urltest-" + RandomUtil.randomSeq(3)
|
||||
this.outbounds.push(createOutbound("urltest", {
|
||||
tag: urlTestTsg,
|
||||
outbounds: this.outbounds.map((o:Outbound) => o.tag),
|
||||
interrupt_exist_connections: false,
|
||||
interval: "30s"
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
// check duplicate tag
|
||||
this.outbounds.forEach((o:Outbound, index:number) => {
|
||||
const isDuplicatedTag = Data().checkTag("outbound",0, o.tag)
|
||||
this.outChecks[index] = isDuplicatedTag ? 2 : 0
|
||||
})
|
||||
|
||||
// save data
|
||||
this.loading = true
|
||||
this.outbounds.forEach(async (o:Outbound, index:number) => {
|
||||
if (this.outChecks[index] == 2) return
|
||||
this.outChecks[index] = 3
|
||||
const success = await Data().save("outbounds", "new", o)
|
||||
if (success) this.outChecks[index] = 1
|
||||
else this.outChecks[index] = 2
|
||||
})
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
newOutboundTags(): string[] {
|
||||
return this.outbounds.map((o:Outbound) => o.tag)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.resetData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="400">
|
||||
<v-card class="rounded-lg" id="qrcode-modal" :loading="loading">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>QrCode</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-skeleton-loader
|
||||
class="mx-auto border"
|
||||
width="80%"
|
||||
type="text, image, divider, text, image"
|
||||
v-if="loading"
|
||||
></v-skeleton-loader>
|
||||
<v-card-text style="overflow-y: auto; padding: 0" :hidden="loading">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
density="compact"
|
||||
fixed-tabs
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="sub">{{ $t('setting.sub') }}</v-tab>
|
||||
<v-tab value="link">{{ $t('client.links') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab" style="margin-top: 10px;">
|
||||
<v-window-item value="sub">
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ $t('setting.sub') }}</v-chip><br />
|
||||
<QrcodeVue :value="clientSub" :size="size" @click="copyToClipboard(clientSub)" :margin="1" style="border-radius: 1rem; cursor: copy;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ $t('setting.jsonSub') }}</v-chip><br />
|
||||
<QrcodeVue :value="clientSub + '?format=json'" :size="size" @click="copyToClipboard(clientSub + '?format=json')" :margin="1" style="border-radius: 1rem; cursor: copy;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ $t('setting.clashSub') }}</v-chip><br />
|
||||
<QrcodeVue :value="clientSub + '?format=clash'" :size="size" @click="copyToClipboard(clientSub + '?format=clash')" :margin="1" style="border-radius: 1rem; cursor: copy;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>SING-BOX (scan only)</v-chip><br />
|
||||
<QrcodeVue :value="singbox" :size="size" :margin="1" style="border-radius: .8rem; cursor: not-allowed;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="link">
|
||||
<v-row v-for="l in clientLinks">
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ l.remark?? $t('client.' + l.type) }}</v-chip><br />
|
||||
<QrcodeVue :value="l.uri" :size="size" @click="copyToClipboard(l.uri)" :margin="1" style="border-radius: .5rem; cursor: copy;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
import Clipboard from 'clipboard'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['id', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
tab: "sub",
|
||||
client: <any>{},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true
|
||||
const newData = await Data().loadClients(this.$props.id)
|
||||
this.client = newData
|
||||
this.loading = false
|
||||
},
|
||||
copyToClipboard(txt:string) {
|
||||
const hiddenButton = document.createElement('button')
|
||||
hiddenButton.className = 'clipboard-btn'
|
||||
document.body.appendChild(hiddenButton)
|
||||
|
||||
const clipboard = new Clipboard('.clipboard-btn', {
|
||||
text: () => txt,
|
||||
container: document.getElementById('qrcode-modal')?? undefined
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
clipboard.destroy()
|
||||
push.success({
|
||||
message: i18n.global.t('success') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
clipboard.destroy()
|
||||
push.error({
|
||||
message: i18n.global.t('failed') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
// Perform click on hidden button to trigger copy
|
||||
hiddenButton.click()
|
||||
document.body.removeChild(hiddenButton)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clientSub() {
|
||||
return Data().subURI + this.client.name
|
||||
},
|
||||
singbox() {
|
||||
const url = Data().subURI + this.client.name + "?format=json"
|
||||
return "sing-box://import-remote-profile?url=" + encodeURIComponent(url) + "#" + this.client.name
|
||||
},
|
||||
clientLinks() {
|
||||
return this.client.links?? []
|
||||
},
|
||||
size() {
|
||||
if (window.innerWidth > 380) return 300
|
||||
if (window.innerWidth > 330) return 280
|
||||
return 250
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.tab = "sub"
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { QrcodeVue }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.rule') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="logical" :label="$t('rule.logical')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto" v-if="logical" justify="center" align="center">
|
||||
<v-btn color="primary" @click="ruleData.rules.push(<rule>{})" hide-details>{{ $t('actions.add') + " " + $t('objects.rule') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card style="background-color: inherit; margin-bottom: 5px;" v-for="(r, index) in ruleData.rules" v-if="ruleData.type == 'logical'">
|
||||
<v-card-subtitle>{{ $t('objects.rule') + ' ' + (Number(index)+1) }}
|
||||
<v-icon @click="ruleData.rules.splice(index,1)" icon="mdi-delete" v-if="ruleData.rules.length>1" />
|
||||
</v-card-subtitle>
|
||||
<v-card-text style="padding: 0;">
|
||||
<RuleOptions
|
||||
:rule="r"
|
||||
:clients="clients"
|
||||
:inTags="inTags"
|
||||
:outTags="outTags"
|
||||
:rsTags="rsTags" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<RuleOptions
|
||||
v-else
|
||||
:rule="ruleData.rules[0]"
|
||||
:clients="clients"
|
||||
:inTags="inTags"
|
||||
:outTags="outTags"
|
||||
:rsTags="rsTags" />
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.action"
|
||||
:items="actions"
|
||||
:label="$t('admin.action')"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="logical">
|
||||
<v-combobox
|
||||
v-model="ruleData.mode"
|
||||
:items="['and', 'or']"
|
||||
:label="$t('rule.mode')"
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="ruleData.invert" :label="$t('rule.invert')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card subtitle="Route" v-if="ruleData.action == 'route'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.outbound"
|
||||
:items="outTags"
|
||||
:label="$t('objects.outbound')"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card subtitle="Route Option" v-if="ruleData.action == 'route-options'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="ruleData.override_address" :label="$t('types.direct.overrideAddr')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model.number="ruleData.override_port"
|
||||
type="number"
|
||||
min="0"
|
||||
max="65534"
|
||||
:label="$t('types.direct.overridePort')"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="ruleData.udp_disable_domain_unmapping" :label="$t('rule.udpDisableDomainUnmapping')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="ruleData.udp_connect" :label="$t('rule.udpConnect')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="ruleData.udp_timeout" :label="$t('rule.udpTimeout')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card subtitle="Reject" v-if="ruleData.action == 'reject'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.method"
|
||||
:items="[{ title: 'Default', value: 'default' },{ title: 'Drop', value: 'drop'}]"
|
||||
:label="$t('rule.method')"
|
||||
clearable
|
||||
@click:clear="delete ruleData.method"
|
||||
hide-details>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="ruleData.no_drop" :label="$t('rule.noDrop')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card subtitle="Sniff" v-if="ruleData.action == 'sniff'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.sniffer"
|
||||
:items="sniffers"
|
||||
:label="$t('rule.sniffer')"
|
||||
multiple
|
||||
chips
|
||||
hide-details>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="ruleData.timeout" :label="$t('rule.timeout')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card subtitle="Resolve" v-if="ruleData.action == 'resolve'">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.strategy"
|
||||
:items="strategies"
|
||||
:label="$t('rule.strategy')"
|
||||
clearable
|
||||
@click:clear="delete ruleData.strategy"
|
||||
hide-details>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="ruleData.server" :label="$t('basic.dns.server')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { logicalRule, rule, actionKeys } from '@/types/rules'
|
||||
import RuleOptions from '@/components/Rule.vue'
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'clients', 'inTags', 'outTags', 'rsTags'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
title: 'add',
|
||||
loading: false,
|
||||
ruleData: <any>{
|
||||
type: 'logical',
|
||||
mode: 'and',
|
||||
rules: <rule[]>[{}],
|
||||
invert: false,
|
||||
action: 'route',
|
||||
outbound: 'direct',
|
||||
},
|
||||
actions: [
|
||||
{ title: 'Route', value: 'route'},
|
||||
{ title: 'Route Options', value: 'route-options'},
|
||||
{ title: 'Bypass', value: 'bypass'},
|
||||
{ title: 'Reject', value: 'reject'},
|
||||
{ title: 'Hijack DNS', value: 'hijack-dns'},
|
||||
{ title: 'Sniff', value: 'sniff'},
|
||||
{ title: 'Resolve', value: 'resolve'}
|
||||
],
|
||||
sniffers: [
|
||||
{ title: 'HTTP', value: 'http' },
|
||||
{ title: 'TLS', value: 'tls' },
|
||||
{ title: 'QUIC', value: 'quic' },
|
||||
{ title: 'STUN', value: 'stun' },
|
||||
{ title: 'DNS', value: 'dns' },
|
||||
{ title: 'BitTorrent', value: 'bittorrent' },
|
||||
{ title: 'DTLS', value: 'dtls' },
|
||||
{ title: 'SSH', value: 'ssh' },
|
||||
{ title: 'RDP', value: 'rdp' },
|
||||
{ title: 'NTP', value: 'ntp' },
|
||||
],
|
||||
strategies: [
|
||||
{ title: 'Prefer IPv4', value: 'prefer_ipv4' },
|
||||
{ title: 'Prefer IPv6', value: 'prefer_ipv6' },
|
||||
{ title: 'IPv4 Only', value: 'ipv4_only' },
|
||||
{ title: 'IPv6 Only', value: 'ipv6_only' },
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
if (newData.type) {
|
||||
this.ruleData = newData
|
||||
} else {
|
||||
this.ruleData = {
|
||||
type: 'simple',
|
||||
mode: 'and',
|
||||
rules: <rule[]>[{}],
|
||||
}
|
||||
Object.keys(newData).forEach(key => {
|
||||
if (actionKeys.includes(key)) {
|
||||
this.ruleData[key] = newData[key]
|
||||
} else {
|
||||
this.ruleData.rules[0][key] = newData[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
this.title = 'edit'
|
||||
}
|
||||
else {
|
||||
this.ruleData = <logicalRule>{
|
||||
type: 'simple',
|
||||
mode: 'and',
|
||||
rules: <rule[]>[{}],
|
||||
invert: false,
|
||||
action: 'route',
|
||||
outbound: this.$props.outTags[0]?? 'direct',
|
||||
}
|
||||
this.title = 'add'
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
let newRule = <any>{
|
||||
action: this.ruleData.action,
|
||||
invert: this.ruleData.invert? this.ruleData.invert : undefined,
|
||||
}
|
||||
|
||||
// Filter action data
|
||||
switch (newRule.action){
|
||||
case 'route':
|
||||
newRule.outbound = this.ruleData.outbound
|
||||
break
|
||||
case 'route-options':
|
||||
newRule.override_address = this.ruleData.override_address?.length > 0 ? this.ruleData.override_address : undefined
|
||||
newRule.override_port = this.ruleData?.override_port > 0 ? this.ruleData.override_port : undefined
|
||||
newRule.network_strategy = this.ruleData.network_strategy?.length > 0 ? this.ruleData.network_strategy : undefined
|
||||
newRule.fallback_delay = this.ruleData.fallback_delay?.length > 0 ? this.ruleData.fallback_delay : undefined
|
||||
newRule.udp_disable_domain_unmapping = this.ruleData.udp_disable_domain_unmapping? true : undefined
|
||||
newRule.udp_connect = this.ruleData.udp_connect? true : undefined
|
||||
newRule.udp_timeout = this.ruleData.udp_timeout?.length > 0 ? this.ruleData.udp_timeout : undefined
|
||||
break
|
||||
case 'reject':
|
||||
newRule.method = this.ruleData.method?.length > 0 ? this.ruleData.method : undefined
|
||||
newRule.no_drop = this.ruleData.no_drop? true : undefined
|
||||
break
|
||||
case 'sniff':
|
||||
newRule.sniffer = this.ruleData.sniffer?.length > 0 ? this.ruleData.sniffer : undefined
|
||||
newRule.timeout = this.ruleData.timeout?.length > 0 ? this.ruleData.timeout : undefined
|
||||
break
|
||||
case 'resolve':
|
||||
newRule.strategy = this.ruleData.strategy?.length > 0 ? this.ruleData.strategy : undefined
|
||||
newRule.server = this.ruleData.server?.length > 0 ? this.ruleData.server : undefined
|
||||
break
|
||||
}
|
||||
|
||||
// Add rules
|
||||
if (this.ruleData.type == 'simple'){
|
||||
newRule = { ...this.ruleData.rules[0], ...newRule }
|
||||
} else {
|
||||
newRule.type = 'logical'
|
||||
newRule.mode = this.ruleData.mode
|
||||
newRule.rules = this.ruleData.rules
|
||||
}
|
||||
this.$emit('save', newRule)
|
||||
this.loading = false
|
||||
},
|
||||
deleteRule(index:number) {
|
||||
this.ruleData.rules.splice(index,1)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
logical: {
|
||||
get() { return this.ruleData.type == 'logical' },
|
||||
set(v:boolean) {
|
||||
this.ruleData.type = v? 'logical' : 'simple'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { RuleOptions }
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-top-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
{{ $t('rule.import.rulesTitle') }}
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-chip v-if="parsed" size="small" color="primary" variant="tonal">
|
||||
{{ parsed.rules?.length ?? 0 }} {{ $t('pages.rules') }} · {{ parsed.rule_set?.length ?? 0 }} {{ $t('rule.ruleset') }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-tabs v-model="tab" @update:modelValue="tabChanged">
|
||||
<v-tab value="json">JSON</v-tab>
|
||||
<v-tab value="file">{{ $t('rule.import.uploadFile') }}</v-tab>
|
||||
<v-tab value="url">{{ $t('rule.import.fromUrl') }}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="json">
|
||||
<v-alert variant="text" type="info">{{ $t('rule.import.jsonHint') }}</v-alert>
|
||||
<v-textarea
|
||||
v-model="rawJson"
|
||||
label="JSON"
|
||||
variant="outlined"
|
||||
rows="12"
|
||||
hide-details
|
||||
spellcheck="false"
|
||||
class="mb-4"
|
||||
:error="!!error"></v-textarea>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="file">
|
||||
<v-alert variant="text" type="info">{{ $t('rule.import.fileJsonHint') }}</v-alert>
|
||||
<v-file-input
|
||||
:label="$t('rule.import.selectJson')"
|
||||
accept=".json"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
prepend-icon="mdi-file-code"
|
||||
clearable
|
||||
@click:clear="tabChanged"
|
||||
@update:modelValue="onFileUpload($event)" />
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="url">
|
||||
<v-alert variant="text" type="info">{{ $t('rule.import.urlHint') }}</v-alert>
|
||||
<v-text-field
|
||||
v-model="fetchUrl"
|
||||
label="URL"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
spellcheck="false"
|
||||
placeholder="https://raw.githubusercontent.com/.../rules.json"
|
||||
append-icon="mdi-download"
|
||||
@keydown.enter="fetchFromUrl"
|
||||
@click:append="fetchFromUrl" />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
<v-alert v-if="error" type="error" variant="text" v-html="error"></v-alert>
|
||||
|
||||
<template v-if="parsed">
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-alert v-if="hasConflicts" type="warning" variant="tonal" :title="$t('rule.import.conflict')" class="mb-4">
|
||||
{{ $t('rule.import.conflictDesc', { rules: existingRulesCount, rulesets: existingRulesetsCount }) }}
|
||||
<v-radio-group v-model="mode" hide-details class="mt-2">
|
||||
<v-radio value="merge" :label="$t('rule.import.merge')" />
|
||||
<v-radio value="replace" :label="$t('rule.import.replace')" />
|
||||
</v-radio-group>
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="parsed.final" type="info" variant="tonal" class="mb-4">
|
||||
{{ $t('rule.import.finalOutbound') }}:
|
||||
<v-chip size="small" color="secondary" variant="tonal">{{ parsed.final }}</v-chip>
|
||||
<v-checkbox v-model="applyFinal" :label="$t('rule.import.applyFinal')" hide-details density="compact" />
|
||||
</v-alert>
|
||||
|
||||
<span class="v-card-subtitle">
|
||||
{{ $t('pages.rules') }}
|
||||
<v-badge v-if="parsed.rules?.length > 0" color="success" :content="parsed.rules?.length" inline />
|
||||
</span>
|
||||
<v-table v-if="parsed.rules?.length" density="compact" class="mb-4" striped="even">
|
||||
<thead>
|
||||
<tr><th>#</th><th>{{ $t('type') }}</th><th>{{ $t('admin.action') }}</th><th>{{ $t('objects.outbound') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(r, i) in parsed.rules" :key="i">
|
||||
<td>{{ (i as number) + 1 }}</td>
|
||||
<td>{{ r.type ?? 'simple' }}</td>
|
||||
<td>{{ r.action }}</td>
|
||||
<td>{{ r.outbound ?? '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<span class="v-card-subtitle">
|
||||
{{ $t('rule.ruleset') }}
|
||||
<v-badge v-if="parsed.rule_set?.length > 0" color="success" :content="parsed.rule_set?.length" inline />
|
||||
<span v-if="skippedRulesets > 0">
|
||||
<v-badge color="warning" :content="skippedRulesets" inline v-tooltip:top="$t('rule.import.skipped')" />
|
||||
</span>
|
||||
</span>
|
||||
<v-table v-if="parsed.rule_set?.length" density="compact" striped="even">
|
||||
<thead>
|
||||
<tr><th>{{ $t('objects.tag') }}</th><th>{{ $t('ruleset.format') }}</th><th>{{ $t('type') }}</th><th>{{ $t('ruleset.interval') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(rs, i) in parsed.rule_set" :key="i"
|
||||
:style="mode === 'merge' && existingRulesetTags.includes(rs.tag) ? 'opacity:0.4' : ''">
|
||||
<td style="font-family: monospace;">{{ rs.tag }}</td>
|
||||
<td>{{ rs.format }}</td>
|
||||
<td>{{ rs.type }}</td>
|
||||
<td>{{ rs.update_interval ?? '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="tab === 'json'"
|
||||
@click="parseJson"
|
||||
variant="tonal"
|
||||
color="success"
|
||||
:disabled="rawJson.trim().length === 0"
|
||||
>
|
||||
{{ $t('rule.import.parse') }}
|
||||
<v-icon icon="mdi-magnify" />
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn @click="close" variant="text">{{ $t('actions.close') }}</v-btn>
|
||||
<v-btn @click="save" color="primary" variant="flat" :disabled="!parsed">
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ["visible", "existingRulesCount", "existingRulesetsCount", "existingRulesetTags"],
|
||||
emits: ['save', 'close'],
|
||||
data() {
|
||||
return {
|
||||
tab: 'json',
|
||||
rawJson: '',
|
||||
fetchUrl: '',
|
||||
fetching: false,
|
||||
error: '',
|
||||
parsed: null as any,
|
||||
mode: 'merge' as 'merge' | 'replace',
|
||||
applyFinal: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasConflicts(): boolean {
|
||||
return this.existingRulesCount > 0 || this.existingRulesetsCount > 0
|
||||
},
|
||||
skippedRulesets(): number {
|
||||
if (!this.parsed?.rule_set) return 0
|
||||
const existing = new Set(this.existingRulesetTags)
|
||||
return this.parsed.rule_set.filter((rs: any) => existing.has(rs.tag)).length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tabChanged() {
|
||||
this.rawJson = ''
|
||||
this.fetchUrl = ''
|
||||
this.error = ''
|
||||
this.parsed = null
|
||||
this.mode = this.hasConflicts ? 'merge' : 'replace'
|
||||
this.applyFinal = false
|
||||
},
|
||||
extractRouteBlock(obj: any): any {
|
||||
if (obj?.route && (obj.route.rules || obj.route.rule_set)) return obj.route
|
||||
if (obj?.rules || obj?.rule_set) return obj
|
||||
return null
|
||||
},
|
||||
setParsed(block: any) {
|
||||
this.parsed = block
|
||||
this.mode = this.hasConflicts ? 'merge' : 'replace'
|
||||
this.applyFinal = false
|
||||
},
|
||||
reset() {
|
||||
this.tab = 'json'
|
||||
this.tabChanged()
|
||||
},
|
||||
parseJson() {
|
||||
this.error = ''
|
||||
this.parsed = null
|
||||
try {
|
||||
const block = this.extractRouteBlock(JSON.parse(this.rawJson))
|
||||
if (!block) {
|
||||
this.error = this.$t('rule.import.errNoArrays')
|
||||
return
|
||||
}
|
||||
this.setParsed(block)
|
||||
} catch (e: any) {
|
||||
this.error = this.$t('rule.import.errJsonParse', { message: e.message })
|
||||
}
|
||||
},
|
||||
async fetchFromUrl() {
|
||||
this.error = ''
|
||||
this.parsed = null
|
||||
this.fetching = true
|
||||
try {
|
||||
const resp = await fetch(this.fetchUrl)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const block = this.extractRouteBlock(await resp.json())
|
||||
if (!block) this.error = this.$t('rule.import.errNoArraysFetched')
|
||||
else this.setParsed(block)
|
||||
} catch (e: any) {
|
||||
this.error = this.$t('rule.import.errFetch', { message: e.message })
|
||||
} finally {
|
||||
this.fetching = false
|
||||
}
|
||||
},
|
||||
async onFileUpload(files: File | File[] | null) {
|
||||
this.error = ''
|
||||
this.parsed = null
|
||||
const file = Array.isArray(files) ? files[0] : files
|
||||
if (!file) {
|
||||
this.error = this.$t('rule.import.errNoFile')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const block = this.extractRouteBlock(JSON.parse(await file.text()))
|
||||
if (!block) {
|
||||
this.error = this.$t('rule.import.errNoArraysInFile')
|
||||
return
|
||||
}
|
||||
this.setParsed(block)
|
||||
} catch (e: any) {
|
||||
this.error = this.$t('rule.import.errJsonParse', { message: e.message })
|
||||
return
|
||||
}
|
||||
},
|
||||
save() {
|
||||
if (!this.parsed) return
|
||||
this.$emit('save', this.parsed, this.mode, this.applyFinal)
|
||||
},
|
||||
close() {
|
||||
this.$emit('close')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v: boolean) {
|
||||
if (v) this.reset()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.ruleset') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="[{title: $t('ruleset.local'), value: 'local'},{ title: $t('ruleset.remote'), value: 'remote'}]"
|
||||
@update:model-value="updateType($event)"
|
||||
v-model="rule_set.type">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="rule_set.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('ruleset.format')"
|
||||
:items="['source', 'binary']"
|
||||
v-model="rule_set.format">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="rule_set.type == 'local'">
|
||||
<v-col cols="12">
|
||||
<v-text-field v-model="rule_set.path" :label="$t('transport.path')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-text-field v-model="rule_set.url" label="URL" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('objects.outbound')"
|
||||
:items="outTags"
|
||||
clearable
|
||||
@click:clear="delete rule_set.download_detour"
|
||||
v-model="rule_set.download_detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="update_intervals" :suffix="$t('date.d')" type="number" min="0" :label="$t('ruleset.interval')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import { ruleset } from '@/types/rules'
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'outTags'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
title: "add",
|
||||
loading: false,
|
||||
rule_set: <ruleset>{},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
this.title = "edit"
|
||||
this.rule_set = <ruleset>JSON.parse(this.$props.data)
|
||||
}
|
||||
else {
|
||||
this.title = "add"
|
||||
this.rule_set = <ruleset>{type: 'local', tag: "rs-" + RandomUtil.randomSeq(3), format: 'binary'}
|
||||
}
|
||||
},
|
||||
updateType(t:string) {
|
||||
if (t == 'local') {
|
||||
delete this.rule_set.url
|
||||
delete this.rule_set.download_detour
|
||||
delete this.rule_set.update_interval
|
||||
} else {
|
||||
delete this.rule_set.path
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.$emit('save', this.rule_set)
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
update_intervals: {
|
||||
get() { return this.rule_set.update_interval != undefined ? parseInt(this.rule_set.update_interval.replace('d','')) : 0 },
|
||||
set(v:number) { this.rule_set.update_interval = v>0 ? v + 'd' : undefined }
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-top-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('rule.import.title') }}</v-col>
|
||||
<v-col cols="auto" v-if="importPreview.length > 0">
|
||||
<v-chip size="small" color="primary" variant="tonal">
|
||||
{{ $t('count') }}: {{ importPreview.length }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-tabs v-model="tab" @update:modelValue="tabChanged">
|
||||
<v-tab value="text">
|
||||
{{ $t('rule.import.pasteUrls') }}
|
||||
</v-tab>
|
||||
<v-tab value="file">
|
||||
{{ $t('rule.import.uploadTxt') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab" class="mb-4">
|
||||
<v-window-item value="text">
|
||||
<v-alert variant="text" type="info">{{ $t('rule.import.urlsHint') }}</v-alert>
|
||||
<v-textarea
|
||||
v-model="importRawText"
|
||||
label="URLs"
|
||||
variant="outlined"
|
||||
rows="10"
|
||||
auto-grow
|
||||
hide-details
|
||||
spellcheck="false"
|
||||
placeholder="https://github.com/.../geoip-telegram.srs https://github.com/.../geosite-youtube.srs"
|
||||
></v-textarea>
|
||||
</v-window-item>
|
||||
<v-window-item value="file">
|
||||
<v-alert variant="text" type="info">{{ $t('rule.import.fileHint') }}</v-alert>
|
||||
<v-file-input
|
||||
:label="$t('rule.import.selectTxt')"
|
||||
accept=".txt"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
prepend-icon="mdi-file-document"
|
||||
@change="onFileUpload"
|
||||
></v-file-input>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('ruleset.format')"
|
||||
:items="['source', 'binary']"
|
||||
v-model="importFormat">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('objects.outbound')"
|
||||
:items="outTags"
|
||||
clearable
|
||||
@click:clear="importDetour=''"
|
||||
v-model="importDetour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="importInterval" :suffix="$t('date.d')" type="number" min="0" :label="$t('ruleset.interval')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template v-if="importPreview.length > 0">
|
||||
<v-divider class="my-4" />
|
||||
<span class="v-card-subtitle">
|
||||
{{ $t('rule.import.preview') }}
|
||||
<v-badge v-if="importPreview.length > 0" color="success" :content="importPreview.length" inline />
|
||||
<v-badge v-if="importSkipped > 0" color="warning" :content="importSkipped" inline v-tooltip:top="$t('rule.import.skipped')" />
|
||||
</span>
|
||||
<v-table density="compact" striped="even" class="mb-4">
|
||||
<thead>
|
||||
<tr><th>{{ $t('objects.tag') }}</th><th>{{ $t('ruleset.format') }}</th><th>URL</th><th>{{ $t('actions.del') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, i) in importPreview" :key="i" :style="item.exists ? 'opacity:0.4' : ''">
|
||||
<td>
|
||||
{{ item.tag }}
|
||||
</td>
|
||||
<td>{{ item.format }}</td>
|
||||
<td v-tooltip:top="item.url" dir="ltr">.../{{ item.url.split('/').pop() ?? item.url }}</td>
|
||||
<td><v-icon icon="mdi-delete" color="error" @click="importPreview.splice(i, 1)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-3">
|
||||
<v-btn @click="parseImport" variant="tonal" :disabled="importRawText.trim().length === 0">
|
||||
<v-icon icon="mdi-magnify" class="mr-1" />{{ $t('rule.import.parse') }}
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn @click="close" variant="text">{{ $t('actions.close') }}</v-btn>
|
||||
<v-btn @click="save" color="primary" variant="flat" :disabled="newCount === 0">
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
interface ImportItem { tag: string; url: string; format: string; exists: boolean }
|
||||
|
||||
export default {
|
||||
props: ["visible", "outTags", "rsTags"],
|
||||
emits: ['save', 'close'],
|
||||
data() {
|
||||
return {
|
||||
tab: 'text',
|
||||
importRawText: '',
|
||||
importFormat: 'binary',
|
||||
importDetour: '',
|
||||
importInterval: 1,
|
||||
importPreview: [] as ImportItem[],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
importSkipped(): number {
|
||||
return this.importPreview.filter(i => i.exists).length
|
||||
},
|
||||
newCount(): number {
|
||||
return this.importPreview.filter(i => !i.exists).length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tabChanged() {
|
||||
this.importPreview = []
|
||||
this.importRawText = ''
|
||||
},
|
||||
urlToTag(url: string): string {
|
||||
try {
|
||||
const filename = new URL(url).pathname.split('/').pop() ?? ''
|
||||
return filename.replace(/\.[^.]+$/, '')
|
||||
} catch {
|
||||
const parts = url.split('/')
|
||||
return parts[parts.length - 1].replace(/\.[^.]+$/, '') || url
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit('close')
|
||||
},
|
||||
parseImport() {
|
||||
const existingTags = new Set(this.rsTags)
|
||||
const seen = new Set<string>()
|
||||
this.importPreview = this.importRawText
|
||||
.split('\n').map(l => l.trim()).filter(l => l.length > 0 && l.startsWith('http'))
|
||||
.filter(url => { if (seen.has(url)) return false; seen.add(url); return true })
|
||||
.map(url => ({ tag: this.urlToTag(url), url, format: this.importFormat, exists: existingTags.has(this.urlToTag(url)) }))
|
||||
},
|
||||
save() {
|
||||
const toAdd = this.importPreview.filter(i => !i.exists).map(item => {
|
||||
const rs: any = { type: 'remote', tag: item.tag, format: item.format, url: item.url }
|
||||
if (this.importDetour) rs.download_detour = this.importDetour
|
||||
if (this.importInterval > 0) rs.update_interval = this.importInterval + 'd'
|
||||
return rs
|
||||
})
|
||||
this.$emit('save', toAdd)
|
||||
},
|
||||
async onFileUpload(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
this.importRawText = await file.text()
|
||||
this.parseImport()
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.tab = 'text'
|
||||
this.tabChanged()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.service') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(srvTypes).map((key,index) => ({title: key, value: Object.values(srvTypes)[index]}))"
|
||||
v-model="srv.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="srv.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<Listen :data="srv" :inTags="inTags" />
|
||||
<Derp v-if="srv.type == srvTypes.DERP" :data="srv" :inTags="inTags" :tsTags="tsTags" />
|
||||
<SSMapi v-if="srv.type == srvTypes.SSMAPI" :data="srv" :ssTags="ssTags" />
|
||||
<Ocm v-if="srv.type == srvTypes.OCM" :data="srv" />
|
||||
<Ccm v-if="srv.type == srvTypes.CCM" :data="srv" />
|
||||
<InTLS v-if="HasTls.includes(srv.type)" :inbound="srv" :tlsConfigs="tlsConfigs" :tls_id="srv.tls_id" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { SrvTypes, createSrv } from '@/types/services'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import Listen from '@/components/Listen.vue'
|
||||
import Derp from '@/components/services/Derp.vue'
|
||||
import Ocm from '@/components/services/Ocm.vue'
|
||||
import Ccm from '@/components/services/Ccm.vue'
|
||||
import InTLS from '@/components/tls/InTLS.vue'
|
||||
import SSMapi from '@/components/services/SSMAPI.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'inTags', 'tsTags', 'ssTags', 'tlsConfigs'],
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
srv: createSrv("derp",{ "tag": "" }),
|
||||
title: "add",
|
||||
tab: "t1",
|
||||
loading: false,
|
||||
srvTypes: SrvTypes,
|
||||
HasTls: [SrvTypes.DERP, SrvTypes.SSMAPI, SrvTypes.OCM, SrvTypes.CCM],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateData(id: number) {
|
||||
if (id > 0) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.srv = createSrv(newData.type, newData)
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
const port = RandomUtil.randomIntRange(10000, 60000)
|
||||
this.srv = createSrv("derp", {
|
||||
tag: "derp-" + RandomUtil.randomSeq(3),
|
||||
listen: '::',
|
||||
listen_port: port,
|
||||
})
|
||||
this.title = "add"
|
||||
}
|
||||
this.tab = "t1"
|
||||
},
|
||||
changeType() {
|
||||
// Tag change only in add service
|
||||
const tag = this.$props.id > 0 ? this.srv.tag : this.srv.type + "-" + RandomUtil.randomSeq(3)
|
||||
// Use previous data
|
||||
const prevConfig = { id: this.srv.id, tag: tag, listen: this.srv.listen, listen_port: this.srv.listen_port }
|
||||
this.srv = createSrv(this.srv.type, prevConfig)
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData(0) // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
async saveChanges() {
|
||||
if (!this.$props.visible) return
|
||||
|
||||
// check duplicate tag
|
||||
const isDuplicatedTag = Data().checkTag("service",this.srv.id, this.srv.tag)
|
||||
if (isDuplicatedTag) return
|
||||
|
||||
// save data
|
||||
this.loading = true
|
||||
const success = await Data().save("services", this.$props.id == 0 ? "new" : "edit", this.srv)
|
||||
if (success) this.closeModal()
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.updateData(this.$props.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Listen, InTLS, Derp, Ocm, Ccm, SSMapi },
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg" :loading="loading">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
{{ $t('stats.graphTitle') }}
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close" @click="$emit('close')"></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<div style="text-align: center; margin: 5px;">
|
||||
{{ $t('objects.' + resource) + " : " + tag }}
|
||||
</div>
|
||||
<v-radio-group v-model="limit" @change="loadData" density="compact" :loading="loading" inline hide-details>
|
||||
<v-radio v-for="p in periods" :label="p.title" :value="p.value"></v-radio>
|
||||
</v-radio-group>
|
||||
<v-container id="container" style="height:40vh;">
|
||||
<v-skeleton-loader
|
||||
class="mx-auto border"
|
||||
width="95%"
|
||||
type="image"
|
||||
v-if="loading"
|
||||
></v-skeleton-loader>
|
||||
<template v-else>
|
||||
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
|
||||
<Line v-if="loaded" :data="usage" :options="<any>options" />
|
||||
</template>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
ChartJS.defaults.font.family = 'Vazirmatn'
|
||||
export default {
|
||||
components: {
|
||||
Line
|
||||
},
|
||||
props: ['visible','resource','tag'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
alert: false,
|
||||
intervalId: <any>0,
|
||||
limit: 1,
|
||||
periods: [
|
||||
{ value: 1, title: i18n.global.n(1) + i18n.global.t('date.h')},
|
||||
{ value: 6, title: i18n.global.n(6) + i18n.global.t('date.h')},
|
||||
{ value: 12, title: i18n.global.n(12) + i18n.global.t('date.h')},
|
||||
{ value: 24, title: i18n.global.n(1) + i18n.global.t('date.d')},
|
||||
{ value: 48, title: i18n.global.n(2) + i18n.global.t('date.d')},
|
||||
{ value: 240, title: i18n.global.n(10) + i18n.global.t('date.d')},
|
||||
{ value: 480, title: i18n.global.n(20) + i18n.global.t('date.d')},
|
||||
{ value: 720, title: i18n.global.n(30) + i18n.global.t('date.d')},
|
||||
{ value: 1440, title: i18n.global.n(60) + i18n.global.t('date.d')},
|
||||
{ value: 2160, title: i18n.global.n(90) + i18n.global.t('date.d')},
|
||||
],
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
elements: {
|
||||
point: { pointStyle: 'crossRot' }
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
text: (ctx:any) => {
|
||||
const {axis = 'xy', intersect, mode} = ctx.chart.options.interaction
|
||||
return 'Mode: ' + mode + ', axis: ' + axis + ', intersect: ' + intersect
|
||||
},
|
||||
footer: (items:any[]) => {
|
||||
return HumanReadable.sizeFormat(items.reduce((acc, c) => acc + c.raw, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: '#777777',
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(label:any, index: number) {
|
||||
return label == 0 ? 0 : HumanReadable.sizeFormat(label,0)
|
||||
},
|
||||
count: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
usage: ref(<any>{}),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/stats', { resource: this.resource, tag: this.tag, limit: this.limit })
|
||||
if (data.success && data.obj) {
|
||||
const obj = <any[]>data.obj
|
||||
const l = String(i18n.global.locale) == 'fa' ? "fa-IR" : "en-US"
|
||||
const oneStep = this.limit * 3600 * 1000 / 360 // Each 10 sec
|
||||
const now = new Date().getTime()
|
||||
const steps = <number[]>[]
|
||||
for (let i = 360; i >= 0; i--) {
|
||||
steps.push(now - (oneStep * i))
|
||||
}
|
||||
const labels = <string[]>[]
|
||||
const uplinkData = <number[]>[]
|
||||
const downlinkData = <number[]>[]
|
||||
for (let i = 1; i<360; i++) {
|
||||
labels.push(this.genLable(steps[i],l))
|
||||
let upSum:number
|
||||
let downSum:number
|
||||
const upTraffics = obj.filter(o => o.direction && o.dateTime*1000 < steps[i] && o.dateTime*1000 > steps[i-1]).map((o:any) => o.traffic)
|
||||
upSum = upTraffics.length>0 ? upTraffics.reduce(u => u) : null
|
||||
const downTraffics = obj.filter(o => !o.direction && o.dateTime*1000 < steps[i] && o.dateTime*1000 > steps[i-1]).map((o:any) => o.traffic)
|
||||
downSum = downTraffics.length>0 ? downTraffics.reduce(d => d) : null
|
||||
uplinkData.push(upSum)
|
||||
downlinkData.push(downSum)
|
||||
}
|
||||
this.usage = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: i18n.global.t('stats.upload'),
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.4)',
|
||||
borderColor: 'rgba(255, 165, 0)',
|
||||
fill: true,
|
||||
data: uplinkData
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('stats.download'),
|
||||
backgroundColor: 'rgba(0, 128, 0, 0.2)',
|
||||
borderColor: 'rgba(0, 128, 0)',
|
||||
fill: true,
|
||||
data: downlinkData
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
this.alert = false
|
||||
} else {
|
||||
this.alert = true
|
||||
this.loaded = false
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
genLable(step:number, locale: string) {
|
||||
return new Date(step).toLocaleString(locale,{
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.limit = 1
|
||||
this.loadData()
|
||||
this.intervalId = setInterval(() => {
|
||||
this.loadData()
|
||||
}, 10000)
|
||||
} else {
|
||||
this.loaded = false
|
||||
this.alert = false
|
||||
this.usage.labels = []
|
||||
if (this.usage.datasets) {
|
||||
this.usage.datasets[0].data = []
|
||||
this.usage.datasets[1].data = []
|
||||
}
|
||||
if (this.intervalId && this.intervalId != 0) {
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,592 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.tls') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-card class="rounded-lg">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('client.name')"
|
||||
hide-details
|
||||
v-model="tls.name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col align="end">
|
||||
<v-btn-toggle v-model="tlsType"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="changeTlsType"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn>TLS</v-btn>
|
||||
<v-btn>Reality</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.server_name != undefined">
|
||||
<v-text-field
|
||||
label="SNI"
|
||||
hide-details
|
||||
v-model="inTls.server_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<template v-if="tlsType == 0">
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.min_version">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.minVer')"
|
||||
:items="tlsVersions"
|
||||
v-model="inTls.min_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.max_version">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.maxVer')"
|
||||
:items="tlsVersions"
|
||||
v-model="inTls.max_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.alpn">
|
||||
<v-select
|
||||
hide-details
|
||||
label="ALPN"
|
||||
multiple
|
||||
:items="alpn"
|
||||
v-model="inTls.alpn">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" v-if="inTls.cipher_suites != undefined">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.cs')"
|
||||
multiple
|
||||
:items="cipher_suites"
|
||||
v-model="inTls.cipher_suites">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<template v-if="tlsType == 0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn-toggle v-model="usePath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="inTls.key=undefined; inTls.certificate=undefined"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="inTls.key_path=undefined; inTls.certificate_path=undefined"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="mdi-key-star"
|
||||
@click="genSelfSigned"
|
||||
:loading="loading">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.generate') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="usePath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.certPath')"
|
||||
hide-details
|
||||
v-model="inTls.certificate_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
v-model="inTls.key_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="certText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
v-model="keyText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.disableSni')" v-model="disableSni" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.insecure')" v-model="insecure" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="outTls.reality && inTls.reality">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.shdwTls.hs')"
|
||||
hide-details
|
||||
v-model="inTls.reality.handshake.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model="server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="mdi-key-star"
|
||||
@click="genRealityKey"
|
||||
:loading="loading">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.generate') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:label="$t('tls.privKey')"
|
||||
hide-details
|
||||
v-model="inTls.reality.private_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:label="$t('tls.pubKey')"
|
||||
hide-details
|
||||
v-model="outTls.reality.public_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
label="Short IDs"
|
||||
hide-details
|
||||
append-icon="mdi-refresh"
|
||||
@click:append="randomSID"
|
||||
v-model="short_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionTime">
|
||||
<v-text-field
|
||||
label="Max Time Diference"
|
||||
type="number"
|
||||
min="1"
|
||||
:suffix="$t('date.m')"
|
||||
hide-details
|
||||
v-model="max_time">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-row v-if="optionStore || optionKtls">
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionStore">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.store')"
|
||||
:items="storeItems"
|
||||
v-model="inTls.store">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<template v-if="optionKtls">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.kernelTx')" v-model="inTls.kernel_tx" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.kernelRx')" v-model="inTls.kernel_rx" hide-details></v-switch>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-row v-if="outTls.utls != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Fingerprint"
|
||||
:items="fingerprints"
|
||||
v-model="outTls.utls.fingerprint">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<template v-if="tlsType == 0">
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionStore" color="primary" :label="$t('tls.store')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionKtls" color="primary" :label="$t('tls.ktls')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTime" color="primary" label="Max Time Difference" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<AcmeVue :tls="inTls" />
|
||||
<EchVue :iTls="inTls" :oTls="outTls" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { tls, iTls, defaultInTls, oTls, defaultOutTls } from '@/types/tls'
|
||||
import AcmeVue from '@/components/tls/Acme.vue'
|
||||
import EchVue from '@/components/tls/Ech.vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
tls: <tls>{ id: 0, name: '', server: <iTls>{ enabled: true }, client: <oTls>{} },
|
||||
title: "add",
|
||||
loading: false,
|
||||
menu: false,
|
||||
tlsType: 0,
|
||||
usePath: 0,
|
||||
alpn: [
|
||||
{ title: "H3", value: 'h3' },
|
||||
{ title: "H2", value: 'h2' },
|
||||
{ title: "Http/1.1", value: 'http/1.1' },
|
||||
],
|
||||
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
|
||||
cipher_suites: [
|
||||
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
|
||||
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
|
||||
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
|
||||
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
|
||||
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
|
||||
],
|
||||
storeItems: [
|
||||
{ title: "Mozilla", value: "mozilla" },
|
||||
{ title: "Chrome", value: "chrome" },
|
||||
],
|
||||
fingerprints: [
|
||||
{ title: "Chrome", value: "chrome" },
|
||||
{ title: "Firefox", value: "firefox" },
|
||||
{ title: "Microsoft Edge", value: "edge" },
|
||||
{ title: "Apple Safari", value: "safari" },
|
||||
{ title: "360", value: "360" },
|
||||
{ title: "QQ", value: "qq" },
|
||||
{ title: "Apple IOS", value: "ios" },
|
||||
{ title: "Android", value: "android" },
|
||||
{ title: "Random", value: "random" },
|
||||
{ title: "Randomized", value: "randomized" },
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData(id: number) {
|
||||
if (id > 0) {
|
||||
const newData = <tls>JSON.parse(this.$props.data)
|
||||
this.tls = newData
|
||||
if (this.tls.server == null) this.tls.server = { enabled: true }
|
||||
if (this.tls.client == null) this.tls.client = {}
|
||||
this.tlsType = newData.server?.reality == undefined ? 0 : 1
|
||||
this.usePath = newData.server?.key == undefined ? 0 : 1
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
this.tls = <tls>{ id: 0, name: '', server: {enabled: true}, client: {} }
|
||||
this.tlsType = 0
|
||||
this.usePath = 0
|
||||
this.title = "add"
|
||||
}
|
||||
},
|
||||
changeTlsType(){
|
||||
if (this.tlsType) {
|
||||
this.tls.server = <iTls>{
|
||||
enabled: true,
|
||||
reality: { enabled: true, handshake: { server_port: 443 }, short_id: RandomUtil.randomShortId() },
|
||||
server_name: ""
|
||||
}
|
||||
this.tls.client = <oTls>{ reality: { public_key: "" }, utls: defaultOutTls.utls }
|
||||
} else {
|
||||
this.tls.server = <iTls>{ enabled: true }
|
||||
this.tls.client = <oTls>{}
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData(0) // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.$emit('save', this.tls)
|
||||
this.loading = false
|
||||
},
|
||||
async genSelfSigned(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "tls", o: this.inTls.server_name?? "''" })
|
||||
this.loading = false
|
||||
if (msg.success) {
|
||||
this.inTls.key_path=undefined
|
||||
this.inTls.certificate_path=undefined
|
||||
this.usePath = 1
|
||||
if (msg.obj.length>0){
|
||||
let privateKey = <string[]>[]
|
||||
let publicKey = <string[]>[]
|
||||
let isPrivateKey = false
|
||||
let isPublicKey = false
|
||||
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (line === "-----BEGIN PRIVATE KEY-----") {
|
||||
isPrivateKey = true
|
||||
isPublicKey = false
|
||||
privateKey.push(line)
|
||||
} else if (line === "-----END PRIVATE KEY-----") {
|
||||
isPrivateKey = false
|
||||
privateKey.push(line)
|
||||
} else if (line === "-----BEGIN CERTIFICATE-----") {
|
||||
isPublicKey = true
|
||||
isPrivateKey = false
|
||||
publicKey.push(line)
|
||||
} else if (line === "-----END CERTIFICATE-----") {
|
||||
isPublicKey = false
|
||||
publicKey.push(line)
|
||||
} else if (isPrivateKey) {
|
||||
privateKey.push(line)
|
||||
} else if (isPublicKey) {
|
||||
publicKey.push(line)
|
||||
}
|
||||
})
|
||||
this.inTls.key = privateKey?? undefined
|
||||
this.inTls.certificate = publicKey?? undefined
|
||||
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
async genRealityKey(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "reality" })
|
||||
this.loading = false
|
||||
if (msg.success) {
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (this.inTls.reality && this.outTls.reality){
|
||||
if (line.startsWith("PrivateKey")){
|
||||
this.inTls.reality.private_key = line.substring(12)
|
||||
}
|
||||
if (line.startsWith("PublicKey")){
|
||||
this.outTls.reality.public_key = line.substring(11)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
},
|
||||
randomSID(){
|
||||
this.short_id = RandomUtil.randomShortId().join(',')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inTls(): iTls {
|
||||
return this.tls.server
|
||||
},
|
||||
outTls(): oTls {
|
||||
return this.tls.client
|
||||
},
|
||||
certText: {
|
||||
get(): string { return this.inTls.certificate ? this.inTls.certificate.join('\n') : '' },
|
||||
set(v:string) { this.inTls.certificate = v.split('\n') }
|
||||
},
|
||||
keyText: {
|
||||
get(): string { return this.inTls.key ? this.inTls.key.join('\n') : '' },
|
||||
set(v:string) { this.inTls.key = v.split('\n') }
|
||||
},
|
||||
disableSni: {
|
||||
get() { return this.outTls.disable_sni ?? false },
|
||||
set(v: boolean) { this.tls.client.disable_sni = v ? true : undefined }
|
||||
},
|
||||
insecure: {
|
||||
get() { return this.outTls.insecure ?? false },
|
||||
set(v: boolean) { this.tls.client.insecure = v ? true : undefined }
|
||||
},
|
||||
server_port: {
|
||||
get() { return this.inTls.reality?.handshake?.server_port ? this.inTls.reality.handshake.server_port : 443 },
|
||||
set(v: any) {
|
||||
if (this.inTls.reality){
|
||||
this.inTls.reality.handshake.server_port = v.length == 0 || v == 0 ? 443 : parseInt(v)
|
||||
}
|
||||
}
|
||||
},
|
||||
short_id: {
|
||||
get() { return this.inTls.reality?.short_id ? this.inTls.reality.short_id.join(',') : undefined },
|
||||
set(v: string) {
|
||||
if (this.inTls.reality){
|
||||
this.inTls.reality.short_id = v.length > 0 ? v.split(',') : []
|
||||
}
|
||||
}
|
||||
},
|
||||
max_time: {
|
||||
get() { return this.inTls?.reality?.max_time_difference ? this.inTls.reality.max_time_difference.replace('m','') : 1 },
|
||||
set(v: number) {
|
||||
if (this.inTls.reality){
|
||||
this.inTls.reality.max_time_difference = v > 0 ? v + 'm' : '1m'
|
||||
}
|
||||
}
|
||||
},
|
||||
optionSNI: {
|
||||
get(): boolean { return this.inTls.server_name != undefined },
|
||||
set(v:boolean) { this.inTls.server_name = v ? '' : undefined }
|
||||
},
|
||||
optionALPN: {
|
||||
get(): boolean { return this.inTls.alpn != undefined },
|
||||
set(v:boolean) { this.inTls.alpn = v ? defaultInTls.alpn : undefined }
|
||||
},
|
||||
optionMinV: {
|
||||
get(): boolean { return this.inTls.min_version != undefined },
|
||||
set(v:boolean) { this.inTls.min_version = v ? defaultInTls.min_version : undefined }
|
||||
},
|
||||
optionMaxV: {
|
||||
get(): boolean { return this.inTls.max_version != undefined },
|
||||
set(v:boolean) { this.inTls.max_version = v ? defaultInTls.max_version : undefined }
|
||||
},
|
||||
optionCS: {
|
||||
get(): boolean { return this.inTls.cipher_suites != undefined },
|
||||
set(v:boolean) { this.inTls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
|
||||
},
|
||||
optionFP: {
|
||||
get(): boolean { return this.outTls.utls != undefined },
|
||||
set(v:boolean) { this.outTls.utls = v ? defaultOutTls.utls : undefined }
|
||||
},
|
||||
optionStore: {
|
||||
get(): boolean { return this.inTls.store != undefined },
|
||||
set(v:boolean) { this.inTls.store = v ? 'mozilla' : undefined }
|
||||
},
|
||||
optionKtls: {
|
||||
get(): boolean { return this.inTls.kernel_tx != undefined || this.inTls.kernel_rx != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.inTls.kernel_tx = false
|
||||
this.inTls.kernel_rx = false
|
||||
} else {
|
||||
delete this.inTls.kernel_tx
|
||||
delete this.inTls.kernel_rx
|
||||
}
|
||||
}
|
||||
},
|
||||
optionEch: {
|
||||
get(): boolean { return this.outTls.ech != undefined },
|
||||
set(v:boolean) { this.outTls.ech = v ? defaultOutTls.ech : undefined }
|
||||
},
|
||||
optionTime: {
|
||||
get(): boolean { return this.inTls?.reality?.max_time_difference != undefined },
|
||||
set(v:boolean) { if (this.inTls.reality) this.inTls.reality.max_time_difference = v ? "1m" : undefined }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.updateData(this.$props.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { AcmeVue, EchVue }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg" :loading="loading">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('admin.api.title') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
v-if="newToken.token.length>0"
|
||||
color="success"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
{{ $t('admin.api.msg') }}
|
||||
<v-text-field
|
||||
readonly
|
||||
variant="outlined"
|
||||
bg-color="warning"
|
||||
append-inner-icon="mdi-content-copy"
|
||||
@click:append-inner="copyToClipboard(newToken.token)"
|
||||
v-model="newToken.token"
|
||||
></v-text-field>
|
||||
</v-alert>
|
||||
<v-table density="compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ $t('admin.api.token') }}</th>
|
||||
<th>{{ $t('client.desc') }}</th>
|
||||
<th>{{ $t('date.expiry') }}</th>
|
||||
<th>{{ $t('actions.del') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(token, index) of tokens" :key="token.id">
|
||||
<td>{{ token.id }}</td>
|
||||
<td>{{ token.token }}</td>
|
||||
<td>{{ token.desc }}</td>
|
||||
<td>{{ dateFormatted(token.expiry) }}</td>
|
||||
<td>
|
||||
<v-menu
|
||||
v-model="delOverlay[index]"
|
||||
:close-on-content-click="false"
|
||||
location="top center"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon
|
||||
class="me-2"
|
||||
color="error"
|
||||
v-bind="props"
|
||||
>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-card :title="$t('actions.del')" rounded="lg">
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>{{ $t('confirm') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="error" variant="outlined" @click="deleteToken(token.id)">{{ $t('yes') }}</v-btn>
|
||||
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<v-btn color="primary" @click="showAddToken()">
|
||||
{{ $t('actions.add') }}
|
||||
</v-btn>
|
||||
<v-dialog v-model="showNewToken" width="300">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('admin.api.token') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="showNewToken = false" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('client.desc')" v-model="newToken.desc"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('date.expiry')" v-model.number="newToken.expiry" min="0" type="number" :suffix="$t('date.d')"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="outlined"
|
||||
@click="showNewToken = false"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="tonal"
|
||||
@click="addToken"
|
||||
>
|
||||
{{ $t('actions.add') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import Clipboard from 'clipboard'
|
||||
import { push } from 'notivue';
|
||||
|
||||
export default {
|
||||
props: ['visible', 'user'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tokens: <any[]>[],
|
||||
showNewToken: false,
|
||||
newToken: {
|
||||
desc: '',
|
||||
token: '',
|
||||
expiry: 0,
|
||||
},
|
||||
delOverlay: new Array<boolean>(0),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
locale() {
|
||||
const l = i18n.global.locale.value
|
||||
switch (l) {
|
||||
case "zhHans":
|
||||
return "zh-cn"
|
||||
case "zhHant":
|
||||
return "zh-tw"
|
||||
default:
|
||||
return l
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/tokens')
|
||||
if (data.success) {
|
||||
this.tokens = data.obj ?? []
|
||||
this.delOverlay = new Array<boolean>(this.tokens.length).fill(false)
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
resetNewToken() {
|
||||
this.newToken={
|
||||
desc: '',
|
||||
token: '',
|
||||
expiry: 30,
|
||||
}
|
||||
},
|
||||
showAddToken() {
|
||||
this.resetNewToken()
|
||||
this.showNewToken = true
|
||||
},
|
||||
async addToken() {
|
||||
this.loading = true
|
||||
this.newToken.expiry = this.newToken.expiry>0 ? this.newToken.expiry : 0
|
||||
const response = await HttpUtils.post('api/addToken', { desc: this.newToken.desc, expiry: this.newToken.expiry })
|
||||
if (response.success) {
|
||||
this.newToken.token = response.obj
|
||||
this.loadData()
|
||||
this.showNewToken = false
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
async deleteToken(id: number) {
|
||||
this.loading = true
|
||||
const response = await HttpUtils.post('api/deleteToken', { id: id })
|
||||
if (response.success) {
|
||||
this.loadData()
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
dateFormatted(expiry: number) {
|
||||
if (expiry == 0) return i18n.global.t('unlimited')
|
||||
const date = new Date(expiry*1000)
|
||||
return date.toLocaleString(this.locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
},
|
||||
copyToClipboard(txt:string) {
|
||||
const hiddenButton = document.createElement('button')
|
||||
hiddenButton.className = 'clipboard-btn'
|
||||
document.body.appendChild(hiddenButton)
|
||||
|
||||
const clipboard = new Clipboard('.clipboard-btn', {
|
||||
text: () => txt,
|
||||
container: document.getElementById('qrcode-modal')?? undefined
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
clipboard.destroy()
|
||||
push.success({
|
||||
message: i18n.global.t('success') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
clipboard.destroy()
|
||||
push.error({
|
||||
message: i18n.global.t('failed') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
// Perform click on hidden button to trigger copy
|
||||
hiddenButton.click()
|
||||
document.body.removeChild(hiddenButton)
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.resetNewToken()
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<v-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" transition="dialog-bottom-transition" width="90%" max-width="400">
|
||||
<v-card class="rounded-lg" :loading="loading">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('main.stats.title') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-icon icon="mdi-refresh" class="me-2" @click="refresh" v-tooltip:top="$t('actions.update')" />
|
||||
<v-icon icon="mdi-close" @click="$emit('update:visible', false)" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-table density="compact">
|
||||
<tbody>
|
||||
<tr v-for="row in tableRows" :key="row.key">
|
||||
<td class="pa-2" style="width: 40px;">
|
||||
<v-icon :icon="row.icon" size="small" :color="row.color || undefined" />
|
||||
</td>
|
||||
<td class="pa-2">{{ row.label }}</td>
|
||||
<td class="pa-2 text-end" style="direction: ltr;">{{ row.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup(props) {
|
||||
const loading = ref(false)
|
||||
const info = ref<{
|
||||
clients?: number
|
||||
inbounds?: number
|
||||
outbounds?: number
|
||||
services?: number
|
||||
endpoints?: number
|
||||
clientUp?: number
|
||||
clientDown?: number
|
||||
}>({})
|
||||
|
||||
const clientUp = computed(() => HumanReadable.sizeFormat(info.value.clientUp ?? 0))
|
||||
const clientDown = computed(() => HumanReadable.sizeFormat(info.value.clientDown ?? 0))
|
||||
const totalUsage = computed(() => {
|
||||
const up = info.value.clientUp ?? 0
|
||||
const down = info.value.clientDown ?? 0
|
||||
return HumanReadable.sizeFormat(up + down)
|
||||
})
|
||||
|
||||
const tableRows = computed(() => {
|
||||
const t = (key: string) => i18n.global.t(key)
|
||||
return [
|
||||
{ key: 'clients', icon: 'mdi-account-multiple', label: t('pages.clients'), value: info.value.clients ?? 0, color: undefined },
|
||||
{ key: 'inbounds', icon: 'mdi-cloud-download', label: t('pages.inbounds'), value: info.value.inbounds ?? 0, color: undefined },
|
||||
{ key: 'outbounds', icon: 'mdi-cloud-upload', label: t('pages.outbounds'), value: info.value.outbounds ?? 0, color: undefined },
|
||||
{ key: 'services', icon: 'mdi-server', label: t('pages.services'), value: info.value.services ?? 0, color: undefined },
|
||||
{ key: 'endpoints', icon: 'mdi-cloud-tags', label: t('pages.endpoints'), value: info.value.endpoints ?? 0, color: undefined },
|
||||
{ key: 'clientUp', icon: 'mdi-cloud-upload', label: t('stats.upload'), value: clientUp.value, color: 'orange' },
|
||||
{ key: 'clientDown', icon: 'mdi-cloud-download', label: t('stats.download'), value: clientDown.value, color: 'success' },
|
||||
{ key: 'totalUsage', icon: 'mdi-chart-box', label: t('main.stats.totalUsage'), value: totalUsage.value, color: 'primary' },
|
||||
]
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
loading.value = true
|
||||
const data = await HttpUtils.get('api/status', { r: 'db' })
|
||||
if (data.success && data.obj) {
|
||||
info.value = data.obj.db ?? data.obj
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(() => props.visible, (v) => {
|
||||
if (v) refresh()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
info,
|
||||
clientUp,
|
||||
clientDown,
|
||||
totalUsage,
|
||||
tableRows,
|
||||
refresh,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="400">
|
||||
<v-card class="rounded-lg" id="qrcode-modal" :loading="loading">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>Wireguard QrCode</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-row v-for="l, i in wgLinks">
|
||||
<v-col style="text-align: center;" v-if="l.length>0">
|
||||
<v-chip>{{ $t('types.wg.peer') + ' ' + (i+1) }}</v-chip> <v-icon icon="mdi-download" @click="download(l,i)" /><br />
|
||||
<QrcodeVue :value="l" :size="size" @click="copyToClipboard(l)" :margin="1" style="border-radius: .5rem; cursor: copy;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['data', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
wgData: <any>{},
|
||||
wgLinks: <string[]>[],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.wgData = this.$props.data
|
||||
this.wgLinks = []
|
||||
const address = location.hostname
|
||||
this.wgData.peers.forEach((_: any, index: number) => {
|
||||
this.wgLinks.push(this.getWireguardLink(index, address))
|
||||
})
|
||||
},
|
||||
getWireguardLink(peerId: number, address: string) {
|
||||
const peerData = this.wgData.peers[peerId]
|
||||
const keys = this.wgData.ext?.keys?.find((key: any) => key.public_key == peerData.public_key)
|
||||
if (!keys || !this.wgData.ext?.public_key) return ''
|
||||
let txt = `[Interface]\n`
|
||||
txt += `PrivateKey = ${keys.private_key}\n`
|
||||
txt += `Address = ${peerData.allowed_ips.join(',')}\n`
|
||||
txt += `DNS = ${this.wgData.ext?.dns?.length>0 ? this.wgData.ext.dns : '1.1.1.1, 9.9.9.9'}\n`
|
||||
if (this.wgData.mtu) {
|
||||
txt += `MTU = ${this.wgData.mtu}\n`
|
||||
}
|
||||
txt += `\n# ${this.wgData.tag} - ${peerId}\n`
|
||||
txt += `[Peer]\n`
|
||||
txt += `PublicKey = ${this.wgData.ext.public_key}\n`
|
||||
txt += `AllowedIPs = 0.0.0.0/0, ::/0\n`
|
||||
txt += `Endpoint = ${address}:${this.wgData.listen_port}\n`
|
||||
if (peerData.pre_shared_key) {
|
||||
txt += `\nPresharedKey = ${peerData.pre_shared_key}`
|
||||
}
|
||||
if (peerData.persistent_keepalive_interval) {
|
||||
txt += `\nPersistentKeepalive = ${peerData.persistent_keepalive_interval}\n`
|
||||
}
|
||||
return txt;
|
||||
},
|
||||
copyToClipboard(txt:string) {
|
||||
const hiddenButton = document.createElement('button')
|
||||
hiddenButton.className = 'clipboard-btn'
|
||||
document.body.appendChild(hiddenButton)
|
||||
|
||||
const clipboard = new Clipboard('.clipboard-btn', {
|
||||
text: () => txt,
|
||||
container: document.getElementById('qrcode-modal')?? undefined
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
clipboard.destroy()
|
||||
push.success({
|
||||
message: i18n.global.t('success') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
clipboard.destroy()
|
||||
push.error({
|
||||
message: i18n.global.t('failed') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
// Perform click on hidden button to trigger copy
|
||||
hiddenButton.click()
|
||||
document.body.removeChild(hiddenButton)
|
||||
},
|
||||
download(text: string, i: number) {
|
||||
let filename = this.wgData.tag + '_peer_' + (i+1) + '.conf';
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
size() {
|
||||
if (window.innerWidth > 380) return 300
|
||||
if (window.innerWidth > 330) return 280
|
||||
return 250
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { QrcodeVue }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,637 @@
|
||||
export default {
|
||||
success: "success",
|
||||
failed: "failed",
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
none: "None",
|
||||
all: "All",
|
||||
loading: "Loading...",
|
||||
confirm: "Are you sure ?",
|
||||
yes: "yes",
|
||||
no: "no",
|
||||
unlimited: "infinite",
|
||||
type: "Type",
|
||||
protocol: "Protocol",
|
||||
submit: "Submit",
|
||||
reset: "Reset",
|
||||
now: "Now",
|
||||
network: "Network",
|
||||
copyToClipboard: "Copy to clipboard",
|
||||
noData: "No data!",
|
||||
invalidLogin: "Invalid Login!",
|
||||
online: "Online",
|
||||
version: "Version",
|
||||
email: "Email",
|
||||
commaSeparated: "(comma separated)",
|
||||
count: "Count",
|
||||
template: "Template",
|
||||
editor: "Editor",
|
||||
error: {
|
||||
dplData: "Duplicate Data",
|
||||
core: "Sing-Box Error",
|
||||
invalidData: "Invalid Data",
|
||||
},
|
||||
theme: {
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
system: "System",
|
||||
},
|
||||
pages: {
|
||||
login: "Login",
|
||||
home: "Home",
|
||||
inbounds: "Inbounds",
|
||||
outbounds: "Outbounds",
|
||||
services: "Services",
|
||||
endpoints: "Endpoints",
|
||||
clients: "Clients",
|
||||
rules: "Rules",
|
||||
tls: "TLS Settings",
|
||||
basics: "Basics",
|
||||
dns: "DNS",
|
||||
admins: "Admins",
|
||||
settings: "Settings",
|
||||
},
|
||||
main: {
|
||||
tiles: "Tiles",
|
||||
gauges: "Gauges",
|
||||
charts: "Charts",
|
||||
infos: "Information",
|
||||
gauge: {
|
||||
cpu: "CPU Gauge",
|
||||
mem: "RAM Gauge",
|
||||
dsk: "Disk Gauge",
|
||||
swp: "Swap Gauge",
|
||||
},
|
||||
chart: {
|
||||
cpu: "CPU Monitor",
|
||||
mem: "RAM Monitor",
|
||||
net: "Network Bandwidth",
|
||||
pnet: "Network Packets",
|
||||
dio: "Disk I/O",
|
||||
},
|
||||
info: {
|
||||
sys: "System Info",
|
||||
sbd: "Sing-Box Info",
|
||||
host: "Host",
|
||||
cpu: "CPU",
|
||||
core: "Core",
|
||||
uptime: "Uptime",
|
||||
startupTime: "Startup time",
|
||||
threads: "Threads",
|
||||
memory: "Memory",
|
||||
running: "Running"
|
||||
},
|
||||
backup: {
|
||||
title: "Backup & Restore",
|
||||
backup: "Download Backup",
|
||||
restore: "Restore Backup",
|
||||
exclStats: "Exclude graphs",
|
||||
exclChanges: "Exclude changes",
|
||||
sbConfig: "Download Sing-Box Config",
|
||||
},
|
||||
stats: {
|
||||
title: "Usage & Counts",
|
||||
totalUsage: "Total Usage",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "Inbound",
|
||||
client: "Client",
|
||||
outbound: "Outbound",
|
||||
endpoint: "Endpoint",
|
||||
config: "Config",
|
||||
rule: "Rule",
|
||||
ruleset: "Ruleset",
|
||||
service: "Service",
|
||||
dnsserver: "DNS Server",
|
||||
dnsrule: "DNS Rule",
|
||||
user: "User",
|
||||
tag: "Tag",
|
||||
listen: "Listen",
|
||||
dial: "Dial",
|
||||
tls: "TLS",
|
||||
multiplex: "Multiplex",
|
||||
transport: "Transport",
|
||||
headers: "Headers",
|
||||
key: "Key",
|
||||
value: "Value",
|
||||
},
|
||||
actions: {
|
||||
action: "Action",
|
||||
add: "Add",
|
||||
addbulk: "Add Bulk",
|
||||
editbulk: "Edit Bulk",
|
||||
delbulk: "Delete Bulk",
|
||||
new: "New",
|
||||
edit: "Edit",
|
||||
del: "Delete",
|
||||
clone: "Clone",
|
||||
test: "Test",
|
||||
testAll: "Test all",
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
submit: "Submit",
|
||||
set: "Set",
|
||||
generate: "Generate",
|
||||
disable: "Disable",
|
||||
close: "Close",
|
||||
restartApp: "Restart App",
|
||||
restartSb: "Restart Singbox",
|
||||
},
|
||||
login: {
|
||||
title: "Login",
|
||||
username: "Username",
|
||||
unRules: "Username can not be empty",
|
||||
password: "Password",
|
||||
pwRules: "Password can not be empty",
|
||||
},
|
||||
menu: {
|
||||
logout: "Logout",
|
||||
},
|
||||
admin: {
|
||||
changeCred: "Change credentials",
|
||||
oldPass: "Current Password",
|
||||
newUname: "New Username",
|
||||
newPass: "New Password",
|
||||
lastLogin: "Last login",
|
||||
date: "Date",
|
||||
time: "Time",
|
||||
changes: "Changes",
|
||||
actor: "Actor",
|
||||
key: "Key",
|
||||
action: "Action",
|
||||
api: {
|
||||
title: "API Tokens",
|
||||
msg: "Please copy the token below and store it somewhere safe. It will not be shown again.",
|
||||
token: "Token",
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
interface: "Interface",
|
||||
sub: "Subscription",
|
||||
addr: "Address",
|
||||
port: "Port",
|
||||
webPath: "Base URI",
|
||||
domain: "Domain",
|
||||
sslKey: "SSL Key Path",
|
||||
sslCert: "SSL Certificate Path",
|
||||
webUri: "Panel URI",
|
||||
sessionAge: "Session Maximum Age",
|
||||
trafficAge: "Traffic Maximum Age",
|
||||
timeLoc: "Timezone Location",
|
||||
subEncode: "Enable Encoding",
|
||||
subInfo: "Enable Client Info",
|
||||
path: "Default Path",
|
||||
update: "Automatic Update Time",
|
||||
subUri: "Subscription URI",
|
||||
jsonSub: "JSON Subscription",
|
||||
toDirect: "Route to Direct",
|
||||
toBlock: "Route to Block",
|
||||
timestamp: "Timestamp",
|
||||
globalDns: "Global DNS",
|
||||
directDns: "Direct DNS",
|
||||
toDirectDns: "Route to Direct DNS",
|
||||
jsonSubOptions: "Other Options",
|
||||
excludePkg: "Exclude Packages",
|
||||
clashSub: "Clash Subscription",
|
||||
mixedPort: "Mixed Inbound Port",
|
||||
tun: "Tun Inbound",
|
||||
},
|
||||
client: {
|
||||
name: "Name",
|
||||
desc: "Description",
|
||||
group: "Group",
|
||||
inboundTags: "Inbound Tags",
|
||||
basics: "Basics",
|
||||
config: "Config",
|
||||
links: "Links",
|
||||
external: "External Link",
|
||||
sub: "External Subscription",
|
||||
delayStart: "Delay Start",
|
||||
autoReset: "Auto Reset",
|
||||
resetDays: "Reset Days",
|
||||
nextReset: "Next Reset",
|
||||
},
|
||||
bulk: {
|
||||
order: "Order",
|
||||
random: "Random",
|
||||
changeLimits: "Change limits",
|
||||
addInbounds: "Add inbounds",
|
||||
removeInbounds: "Remove inbounds",
|
||||
addDays: "Add days",
|
||||
addVolume: "Add volume",
|
||||
},
|
||||
types: {
|
||||
un: "Username",
|
||||
pw: "Password",
|
||||
direct: {
|
||||
overrideAddr: "Override Address",
|
||||
overridePort: "Override Port",
|
||||
},
|
||||
hy: {
|
||||
obfs: "Obfuscated Password",
|
||||
auth: "Authentication Password",
|
||||
hyOptions: "Hysteria Options",
|
||||
hy2Options: "Hysteria2 Options",
|
||||
ignoreBw: "Ignore Client Bandwidth",
|
||||
},
|
||||
shdwTls: {
|
||||
hs: "Handshake Server",
|
||||
addHS: "Add Handshake Server",
|
||||
},
|
||||
ssh: {
|
||||
passphrase: "Passphrase",
|
||||
hostKey: "Host Keys",
|
||||
algorithm: "Key Algorithms",
|
||||
clientVer: "Client Version",
|
||||
options: "SSH Options",
|
||||
},
|
||||
tor: {
|
||||
execPath: "Executable File Path",
|
||||
dataDir: "Data Directory",
|
||||
extArgs: "Extra Args",
|
||||
},
|
||||
tuic: {
|
||||
congControl: "Congestion Control",
|
||||
authTimeout: "Authentication Timeout",
|
||||
hb: "Heartbeat",
|
||||
},
|
||||
tun: {
|
||||
addr: "Addresses",
|
||||
ifName: "Interface Name",
|
||||
excludeMptcp: "Exclude MPTCP",
|
||||
fallbackRuleIndex: "iproute2 Fallback Rule Index",
|
||||
},
|
||||
vless: {
|
||||
flow: "Flow",
|
||||
udpEnc: "UDP Packet Encoding",
|
||||
},
|
||||
vmess: {
|
||||
security: "Security",
|
||||
globalPadding: "Global Padding",
|
||||
authLen: "Encryptrd Length",
|
||||
},
|
||||
wg: {
|
||||
privKey: "Private Key",
|
||||
pubKey: "Peer Public Key",
|
||||
psk: "Pre-Shared Key",
|
||||
localIp: "Local IPs",
|
||||
worker: "Workers",
|
||||
ifName: "Interface Name",
|
||||
sysIf: "System Interface",
|
||||
options: "Wireguard Options",
|
||||
allowedIp: "Allowed IPs",
|
||||
peer: "Peer",
|
||||
peers: "Peers",
|
||||
},
|
||||
lb: {
|
||||
defaultOut: "Default Outbound",
|
||||
interruptConn: "Interrupt exist connections",
|
||||
testUrl: "Test URL",
|
||||
interval: "Interval",
|
||||
tolerance: "Tolerance",
|
||||
urlTestOptions: "URLTest Options"
|
||||
},
|
||||
ts: {
|
||||
options: "Tailscale Options",
|
||||
stateDir: "State Directory",
|
||||
authKey: "Authentication Key",
|
||||
relayServer: "Relay Server",
|
||||
relayServerPort: "Relay Server Port",
|
||||
relayEndpoints: "Relay Static Endpoints",
|
||||
systemInterface: "System Interface",
|
||||
sysIfName: "Interface Name",
|
||||
sysIfMtu: "Interface MTU",
|
||||
controlUrl: "Control URL",
|
||||
ephemeral: "Ephemeral Mode",
|
||||
hostname: "Hostname",
|
||||
acceptRoutes: "Accept Routes",
|
||||
exitNode: "Exit Node",
|
||||
allowLanAccess: "Allow LAN Access",
|
||||
advRoutes: "Advertise Routes",
|
||||
advExitNode: "Advertise Exit Node",
|
||||
udpTimeout: "UDP Timeout",
|
||||
},
|
||||
ocm: {
|
||||
credentialPath: "Credential Path",
|
||||
usagesPath: "Usages Path",
|
||||
users: "Users",
|
||||
userName: "Name",
|
||||
userToken: "Token",
|
||||
},
|
||||
ccm: {
|
||||
credentialPath: "Credential Path",
|
||||
usagesPath: "Usages Path",
|
||||
users: "Users",
|
||||
userName: "Name",
|
||||
userToken: "Token",
|
||||
},
|
||||
derp: {
|
||||
configPath: "Config Path",
|
||||
verifyClientEndpoint: "Verify Client Endpoint",
|
||||
verifyClientUrl: "Verify Client URL",
|
||||
meshWith: "Mesh With",
|
||||
meshPsk: "Mesh PSK",
|
||||
meshPskFile: "Mesh PSK File",
|
||||
stun: "STUN Server",
|
||||
options: "DERP Options",
|
||||
},
|
||||
anytls: {
|
||||
idleInterval: "Idle Session Check Interval",
|
||||
idleTimeout: "Idle Session Timeout",
|
||||
minIdle: "Minimum Idle Session",
|
||||
},
|
||||
naive: {
|
||||
insecureConcurrency: "Insecure Concurrency",
|
||||
quic: "QUIC",
|
||||
quicCongestion: "QUIC Congestion Control",
|
||||
udpOverTcp: "UDP over TCP",
|
||||
},
|
||||
},
|
||||
in: {
|
||||
addr: "Address",
|
||||
port: "Port",
|
||||
ssMethod: "Method",
|
||||
ssManageable: "Manageable",
|
||||
sSide: "Server Side",
|
||||
cSide: "Client Side",
|
||||
multiDomain: "Multi Domain",
|
||||
remark: "Remark",
|
||||
mdOption: "Multi Domain Options",
|
||||
},
|
||||
listen: {
|
||||
options: "Listen Options",
|
||||
tcpOptions: "TCP Options",
|
||||
udpOptions: "UDP Options",
|
||||
detour: "Detour",
|
||||
detourText: "Forward to inbound",
|
||||
disableTcpKeepAlive: "Disable TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "TCP Keep Alive Interval",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "Bind to Network Interface",
|
||||
bindIp4: "Bind to IPv4",
|
||||
bindIp6: "Bind to IPv6",
|
||||
bindNoPort: "Bind Address No Port",
|
||||
reuseAddr: "Reuse Listener Address",
|
||||
connTimeout: "Connection Timeout",
|
||||
disableTcpKeepAlive: "Disable TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "TCP Keep Alive Interval",
|
||||
domainResolver: "Domain Resolver",
|
||||
options: "Dial Options",
|
||||
detourText: "Forward to outbound",
|
||||
},
|
||||
transport: {
|
||||
enable: "Enable Transport",
|
||||
host: "Host",
|
||||
hosts: "Hosts",
|
||||
path: "Path",
|
||||
httpMethod: "Request Method",
|
||||
idleTimeout: "Idle Timeout",
|
||||
pingTimeout: "Ping Timeout",
|
||||
grpcServiceName: "Service Name",
|
||||
grpcPws: "Permit Without Stream",
|
||||
},
|
||||
mux: {
|
||||
enable: "Enable Multiplex",
|
||||
maxConn: "Max Connections",
|
||||
minStr: "Min Streams",
|
||||
maxStr: "Max Streams",
|
||||
padding: "Only padding",
|
||||
enableBrutal: "Enable Brutal",
|
||||
},
|
||||
out: {
|
||||
addr: "Server Address",
|
||||
port: "Server Port",
|
||||
addUrlTest: "Add URLTest",
|
||||
delay: "Delay",
|
||||
},
|
||||
rule: {
|
||||
add: "Add Rule",
|
||||
simple: "Simple",
|
||||
logical: "Logical",
|
||||
mode: "Mode",
|
||||
invert: "Invert",
|
||||
ipVer: "IP Version",
|
||||
domain: "Domains",
|
||||
domainSufix: "Domain Suffixes",
|
||||
domainKw: "Domain Keywords",
|
||||
domainRgx: "Domain Regexes",
|
||||
ip: "IP CIDRs",
|
||||
privateIp: "Invalid IP Ranges",
|
||||
port: "Ports",
|
||||
portRange: "Port Ranges",
|
||||
srcCidr: "Source IP CIDRs",
|
||||
srcPrivateIp: "Invalid Source IPs",
|
||||
srcPort: "Source Ports",
|
||||
srcPortRange: "Source Port Ranges",
|
||||
ruleset: "Rulesets",
|
||||
rulesetMatchSrc: "Ruleset IPcidr Match Source",
|
||||
preferredBy: "Preferred By (Outbound)",
|
||||
interfaceAddr: "Interface Address",
|
||||
options: "Rule Options",
|
||||
domainRules: "Domain/IP",
|
||||
srcIpRules: "Source IP",
|
||||
srcPortRules: "Source Port",
|
||||
udpDisableDomainUnmapping: "UDP Disable Domain Unmapping",
|
||||
udpConnect: "UDP Connect",
|
||||
udpTimeout: "UDP Timeout",
|
||||
method: "Method",
|
||||
noDrop: "No Drop",
|
||||
sniffer: "Sniffer",
|
||||
timeout: "Timeout",
|
||||
strategy: "Strategy",
|
||||
etaHint: "Enter one item per line. Blank lines and duplicate entries will be skipped.",
|
||||
import: {
|
||||
title: "Mass import of rulesets",
|
||||
rulesTitle: "Importing rules",
|
||||
urlsHint: "One URL per line. The tag is determined from the file name without the extension.",
|
||||
fileHint: "Upload it .a txt file with URLs, one per line.",
|
||||
jsonHint: "Insert a JSON object with the rules and/or rule_set arrays. You can insert the entire \"route\" block: {'{'}...{'}'} or just its contents.",
|
||||
fileJsonHint: "Upload it .a json file with the route block.",
|
||||
urlHint: "Specify a direct link to the JSON file (for example, a raw GitHub link).",
|
||||
preview: "Preview",
|
||||
skipped: "already exist, shown in gray",
|
||||
conflict: "Conflicts detected",
|
||||
merge: "Merge - add imported rules (skip duplicate set tags)",
|
||||
replace: "Replace - delete existing rules and sets, and import them again",
|
||||
pasteUrls: "Insert URL",
|
||||
uploadTxt: "Upload .txt",
|
||||
uploadFile: "Upload a file",
|
||||
fromUrl: "By URL",
|
||||
selectTxt: "Choose .txt file",
|
||||
selectJson: "Choose .json file",
|
||||
parse: "Parse",
|
||||
conflictDesc: "The config already has {rules} of rules and {rulesets} of rule sets. Select an action:",
|
||||
finalOutbound: "Outbound by default (final)",
|
||||
applyFinal: "Apply as outbound by default",
|
||||
errNoArrays: 'No "rules" or "rule_set" arrays found.',
|
||||
errJsonParse: "JSON parse error: {message}",
|
||||
errNoArraysFetched: 'No "rules" or "rule_set" found in fetched JSON.',
|
||||
errFetch: "Fetch error: {message}",
|
||||
errNoFile: "No file selected.",
|
||||
errNoArraysInFile: 'No "rules" or "rule_set" found in the file.',
|
||||
},
|
||||
},
|
||||
ruleset: {
|
||||
add: "Add Ruleset",
|
||||
format: "Data Format",
|
||||
interval: "Update Intervals",
|
||||
remote: "Remote",
|
||||
local: "Local",
|
||||
},
|
||||
dns: {
|
||||
add: "Add Dns Server",
|
||||
title: "Dns Servers",
|
||||
final: "Final",
|
||||
server: "Server",
|
||||
firstServer: "First Server",
|
||||
cacheCapacity: "Cache Capacity",
|
||||
disableCache: "Disable Cache",
|
||||
disableExpire: "Disable Expire",
|
||||
independentCache: "Independent Cache",
|
||||
reverseMapping: "Reverse Mapping",
|
||||
domainStrategy: "Domain Strategy",
|
||||
local: {
|
||||
preferGo: "Prefer Go",
|
||||
},
|
||||
rule: {
|
||||
add: "Add Dns Rule",
|
||||
title: "Dns Rules",
|
||||
inet4Range: "IPv4 Range",
|
||||
inet6Range: "IPv6 Range",
|
||||
acceptDefault: "Accept Default Resolvers",
|
||||
action: {
|
||||
title: "Action",
|
||||
route: "Route",
|
||||
routeOptions: "Route Options",
|
||||
reject: "Reject",
|
||||
predefined: "Predefined",
|
||||
rewriteTtl: "Rewrite TTL",
|
||||
clientSubnet: "Client Subnet",
|
||||
rcode: "Response Code",
|
||||
rcodes: {
|
||||
noError: "Ok",
|
||||
formerr: "Bad request",
|
||||
servFail: "Server failure",
|
||||
nxDomain: "Not found",
|
||||
refused: "Refused",
|
||||
notImp: "Not Implemented",
|
||||
},
|
||||
answer: "Answers",
|
||||
ns: "Nameservers",
|
||||
extra: "Extra",
|
||||
},
|
||||
}
|
||||
},
|
||||
basic: {
|
||||
log: {
|
||||
title: "Logs",
|
||||
level: "Level",
|
||||
output: "Output",
|
||||
timestamp: "Enable Timestamp",
|
||||
},
|
||||
routing: {
|
||||
title: "Routing",
|
||||
defaultOut: "Default Outbound",
|
||||
defaultIf: "Default NIC",
|
||||
defaultRm: "Default Routing Mark",
|
||||
defaultDns: "Default DNS Resolver",
|
||||
autoBind: "Auto Bind NIC",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "Store Fake IP",
|
||||
extController: "External Controller",
|
||||
extUi: "External UI",
|
||||
extUiDownloadUrl: "UI Download URL",
|
||||
extUiDownloadDetour: "UI Download detour",
|
||||
secret: "Secret",
|
||||
defaultMode: "Default Mode",
|
||||
allowOrigin: "Allow Origin",
|
||||
allowPrivate: "Allow Private Network",
|
||||
},
|
||||
},
|
||||
tls: {
|
||||
enable: "Enable TLS",
|
||||
usePath: "Use Path",
|
||||
useText: "Use Text",
|
||||
certPath: "Certificate File Path",
|
||||
keyPath: "Key File Path",
|
||||
cert: "Certificate",
|
||||
key: "Key",
|
||||
options: "TLS Options",
|
||||
minVer: "Minimum Version",
|
||||
maxVer: "Maximum Version",
|
||||
cs: "Cipher suits",
|
||||
privKey: "Private Key",
|
||||
pubKey: "Public Key",
|
||||
disableSni: "Disable SNI",
|
||||
insecure: "Allow Insecure",
|
||||
fragment: "Fragment",
|
||||
fragmentDelay: "Fragment Fallback Delay",
|
||||
recordFragment: "Multiple records Fragmentation",
|
||||
store: "Root Store",
|
||||
ktls: "Kernel TLS",
|
||||
kernelTx: "TX",
|
||||
kernelRx: "RX",
|
||||
queryServerName: "ECH Query Server Name",
|
||||
acme: {
|
||||
options: "ACME Options",
|
||||
dataDir: "Data Directory",
|
||||
defaultDomain: "Default Domain",
|
||||
disableChallenges: "Disable Challenges",
|
||||
httpChallenge: "Disable HTTP Challenge",
|
||||
tlsChallenge: "Disable TLS Challenge",
|
||||
altPorts: "Alternative Ports",
|
||||
altHport: "Alternative HTTP Port",
|
||||
altTport: "Alternative TLS Port",
|
||||
caProvider: "CA Provider",
|
||||
customCa: "Custom CA Provider",
|
||||
extAcc: "External Account",
|
||||
dns01: "DNS01 Challenge",
|
||||
dns01Provider: "DNS01 Challenge Provider",
|
||||
dns01Params: {
|
||||
api_token: "API Token",
|
||||
zone_token: "Zone Token",
|
||||
access_key_id: "Access Key ID",
|
||||
access_key_secret: "Access Key Secret",
|
||||
region_id: "Region ID",
|
||||
security_token: "Security Token",
|
||||
username: "Username",
|
||||
password: "Password",
|
||||
subdomain: "Subdomain",
|
||||
server_url: "Server URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "Upload",
|
||||
download: "Download",
|
||||
volume: "Volume",
|
||||
usage: "Usage",
|
||||
enable: "Enable Statistics",
|
||||
graphTitle: "Traffic Chart",
|
||||
B: "B",
|
||||
KB: "KB",
|
||||
MB: "MB",
|
||||
GB: "GB",
|
||||
TB: "TB",
|
||||
PB: "PB",
|
||||
p: "p",
|
||||
Kp: "Kp",
|
||||
Mp: "Mp",
|
||||
Gp: "Gp",
|
||||
Mbps: "Mbps",
|
||||
},
|
||||
date: {
|
||||
expiry: "Expiry",
|
||||
expired: "Expired",
|
||||
d: "d",
|
||||
h: "h",
|
||||
m: "m",
|
||||
s: "s",
|
||||
ms: "ms",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
export default {
|
||||
success: "موفق",
|
||||
failed: "خطا",
|
||||
enable: "فعال",
|
||||
disable: "غیرفعال",
|
||||
none: "هیچ",
|
||||
all: "همه",
|
||||
loading: "در حال بارگذاری...",
|
||||
confirm: "آیا مطمئن هستید ؟",
|
||||
yes: "بله",
|
||||
no: "خیر",
|
||||
unlimited: "نامحدود",
|
||||
type: "مدل",
|
||||
protocol: "پروتکل",
|
||||
submit: "تایید",
|
||||
reset: "ریست",
|
||||
now: "اکنون",
|
||||
network: "شبکه",
|
||||
copyToClipboard: "کپی در حافظه",
|
||||
noData: "بدون داده!",
|
||||
invalidLogin: "ورود نامعتبر!",
|
||||
online: "آنلاین",
|
||||
version: "نسخه",
|
||||
email: "ایمیل",
|
||||
commaSeparated: "(جداشده با کاما)",
|
||||
count: "تعداد",
|
||||
template: "الگو",
|
||||
editor: "ویرایشگر",
|
||||
error: {
|
||||
dplData: "داده تکراری",
|
||||
core: "خطا در سینگباکس",
|
||||
invalidData: "داده نامعتبر",
|
||||
},
|
||||
theme: {
|
||||
light: "روشن",
|
||||
dark: "تیره",
|
||||
system: "سیستم",
|
||||
},
|
||||
pages: {
|
||||
login: "ورود",
|
||||
home: "خانه",
|
||||
inbounds: "ورودیها",
|
||||
outbounds: "خروجیها",
|
||||
endpoints: "درگاهها",
|
||||
services: "خدمات",
|
||||
clients: "کاربران",
|
||||
rules: "قوانین",
|
||||
tls: "رمزنگاریها",
|
||||
basics: "ترازها",
|
||||
dns: "DNS",
|
||||
admins: "ادمینها",
|
||||
settings: "پیکربندی",
|
||||
},
|
||||
main: {
|
||||
tiles: "کاشیها",
|
||||
gauges: "سنجشها",
|
||||
charts: "نمودارها",
|
||||
infos: "دادهها",
|
||||
gauge: {
|
||||
cpu: "سنجش پردازنده",
|
||||
mem: "سنجش حافظه",
|
||||
dsk: "سنجش دیسک",
|
||||
swp: "سنجش Swap",
|
||||
},
|
||||
chart: {
|
||||
cpu: "نمودار پردازنده",
|
||||
mem: "نمودار حافظه",
|
||||
net: "ترافیک شبکه",
|
||||
pnet: "بستههای شبکه",
|
||||
dio: "نمودار دیسک",
|
||||
},
|
||||
info: {
|
||||
sys: "دادههای سیستم",
|
||||
sbd: "دادههای سینگباکس",
|
||||
host: "نام",
|
||||
cpu: "پردازنده",
|
||||
core: "هسته",
|
||||
uptime: "مدت",
|
||||
startupTime: "زمان راهاندازی",
|
||||
threads: "نخها",
|
||||
memory: "حافظه",
|
||||
running: "اجرا"
|
||||
},
|
||||
backup: {
|
||||
title: "پشتیبانگیری و بازیابی",
|
||||
backup: "دریافت پشتیبان",
|
||||
restore: "بازیابی پشتیبان",
|
||||
exclStats: "بدون گرافها",
|
||||
exclChanges: "بدون تغییرات",
|
||||
sbConfig: "دریافت پیکربندی سینگباکس",
|
||||
},
|
||||
stats: {
|
||||
title: "آمار و تعداد",
|
||||
totalUsage: "مجموع مصرف",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "ورودی",
|
||||
client: "کاربر",
|
||||
outbound: "خروجی",
|
||||
endpoint: "درگاه",
|
||||
config: "پیکربندی",
|
||||
rule: "قانون",
|
||||
ruleset: "مجموعه",
|
||||
service: "خدمت",
|
||||
dnsserver: "سرور DNS",
|
||||
dnsrule: "قانون DNS",
|
||||
user: "کاربر",
|
||||
tag: "برچسب",
|
||||
listen: "گوشدادن",
|
||||
dial: "تماس",
|
||||
tls: "رمزنگاری",
|
||||
multiplex: "تسهیم",
|
||||
transport: "انتقال",
|
||||
headers: "سربرگها",
|
||||
key: "نام",
|
||||
value: "مقدار",
|
||||
},
|
||||
actions: {
|
||||
action: "فرمان",
|
||||
add: "ایجاد",
|
||||
addbulk: "ایجاد انبوه",
|
||||
editbulk: "ویرایش انبوه",
|
||||
delbulk: "حذف انبوه",
|
||||
new: "جدید",
|
||||
edit: "ویرایش",
|
||||
del: "حذف",
|
||||
clone: "شبیهسازی",
|
||||
test: "تست",
|
||||
testAll: "تست همه",
|
||||
save: "ذخیره",
|
||||
update: "بروزرسانی",
|
||||
submit: "ارسال",
|
||||
set: "تنظیم",
|
||||
generate: "تولید",
|
||||
disable: "غیرفعال",
|
||||
close: "بستن",
|
||||
restartApp: "ریستارت پنل",
|
||||
restartSb: "ریستارت سینگباکس",
|
||||
},
|
||||
login: {
|
||||
title: "ورود",
|
||||
username: "نام کاربری",
|
||||
unRules: "نام کاربری نمیتواند خالی باشد",
|
||||
password: "کلمه عبور",
|
||||
pwRules: "کلمه عبور نمیتواند خالی باشد",
|
||||
},
|
||||
menu: {
|
||||
logout: "خروج",
|
||||
},
|
||||
admin: {
|
||||
changeCred: "ویرایش دادهها",
|
||||
oldPass: "رمز کنونی",
|
||||
newUname: "نام کاربری جدید",
|
||||
newPass: "رمز جدید",
|
||||
lastLogin: "آخرین ورود",
|
||||
date: "تاریخ",
|
||||
time: "ساعت",
|
||||
changes: "تغییرات",
|
||||
actor: "مجری",
|
||||
key: "کلید",
|
||||
action: "عمل",
|
||||
api: {
|
||||
title: "توکنهای API",
|
||||
msg: "لطفا توکن زیر را کپی کنید و در یک مکان امن نگهدارید. این توکن دیگر نمایش داده نخواهد شد.",
|
||||
token: "توکن",
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
interface: "نما",
|
||||
sub: "سابسکریپشن",
|
||||
addr: "آدرس",
|
||||
port: "پورت",
|
||||
webPath: "مسیر پایه",
|
||||
domain: "دامنه",
|
||||
sslKey: "مسیر فایل کلید",
|
||||
sslCert: "مسیر فایل گواهی",
|
||||
webUri: "آدرس نهایی پنل",
|
||||
sessionAge: "بیشینه زمان لاگین ماندن",
|
||||
trafficAge: "بیشینه زمان ذخیره ترافیک",
|
||||
timeLoc: "منطقه زمانی",
|
||||
subEncode: "رمزگذاری",
|
||||
subInfo: "نمایش اطلاعات کاربر",
|
||||
path: "مسیر پیشفرض",
|
||||
update: "زمان بروزرسانی خودکار",
|
||||
subUri: "آدرس نهایی سابسکریپشن",
|
||||
jsonSub: "سابسکریپشن JSON",
|
||||
toDirect: "هدایت مستقیم",
|
||||
toBlock: "بستن مسیر",
|
||||
timestamp: "نمایش زمان",
|
||||
globalDns: "DNS کلی",
|
||||
directDns: "DNS مستقیم",
|
||||
toDirectDns: "هدایت به DNS مستقیم",
|
||||
jsonSubOptions: "گزینههای دیگر",
|
||||
excludePkg: "نرمافزارهای استثنا",
|
||||
clashSub: "سابسکریپشن CLASH",
|
||||
mixedPort: "ورودی Mixed",
|
||||
tun: "ورودی TUN",
|
||||
},
|
||||
client: {
|
||||
name: "نام",
|
||||
desc: "شرح",
|
||||
group: "گروه",
|
||||
inboundTags: "برچسبهای ورودی",
|
||||
basics: "پایه",
|
||||
config: "تنظیم",
|
||||
links: "لینکها",
|
||||
external: "لینک خارجی",
|
||||
sub: "سابسکریپشن خارجی",
|
||||
delayStart: "تأخیر شروع",
|
||||
autoReset: "بازنشانی خودکار",
|
||||
resetDays: "روزهای بازنشانی",
|
||||
nextReset: "بازنشانی بعدی",
|
||||
},
|
||||
bulk: {
|
||||
order: "ترتیب",
|
||||
random: "تصادفی",
|
||||
changeLimits: "تغییر محدودیتها",
|
||||
addInbounds: "افزودن اینباندها",
|
||||
removeInbounds: "حذف اینباندها",
|
||||
addDays: "افزودن روز",
|
||||
addVolume: "افزودن حجم",
|
||||
},
|
||||
types: {
|
||||
un: "نام کاربری",
|
||||
pw: "رمز",
|
||||
direct: {
|
||||
overrideAddr: "جایگزین آدرس",
|
||||
overridePort: "جایگزین پورت",
|
||||
},
|
||||
hy: {
|
||||
obfs: "رمز مبهم کننده",
|
||||
auth: "رمز احراز هویت",
|
||||
hyOptions: "گزینههای Hysteria",
|
||||
hy2Options: "گزینههای Hysteria2",
|
||||
ignoreBw: "نادیدهگرفتن پهنایباند کاربر",
|
||||
},
|
||||
shdwTls: {
|
||||
hs: "سرور دستتکانی",
|
||||
addHS: "افزودن سرور دستتکانی",
|
||||
},
|
||||
ssh: {
|
||||
passphrase: "عبارت عبور",
|
||||
hostKey: "کلیدهای هاستها",
|
||||
algorithm: "الگوریتمها",
|
||||
clientVer: "نسخه کلاینت",
|
||||
options: "گزینههای SSH",
|
||||
},
|
||||
tor: {
|
||||
execPath: "مسیر فایل اجرایی",
|
||||
dataDir: "پوشه دادهها",
|
||||
extArgs: "آرگومانهای اضافی",
|
||||
},
|
||||
tuic: {
|
||||
congControl: "کنترل ازدحام",
|
||||
authTimeout: "مهلت احراز هویت",
|
||||
hb: "ضربان قلب",
|
||||
},
|
||||
tun: {
|
||||
addr: "آدرسها",
|
||||
ifName: "نام اینترفیس",
|
||||
excludeMptcp: "حذف MPTCP",
|
||||
fallbackRuleIndex: "شاخص قوانین fallback در iproute2",
|
||||
},
|
||||
vless: {
|
||||
flow: "جریان",
|
||||
udpEnc: "کدگذاری بسته UDP",
|
||||
},
|
||||
vmess: {
|
||||
security: "امنیت",
|
||||
globalPadding: "لایه بندی کلی",
|
||||
authLen: "رمزگذاری اندازه بسته",
|
||||
},
|
||||
wg: {
|
||||
privKey: "کلید خصوصی",
|
||||
pubKey: "کلید عمومی همتا",
|
||||
psk: "کلید مشترک",
|
||||
localIp: "آدرسهای محلی",
|
||||
worker: "عملگرها",
|
||||
ifName: "نام اینترفیس",
|
||||
sysIf: "استفاده از اینترفیس سیستم",
|
||||
options: "گزینههای Wireguard",
|
||||
allowedIp: "آدرسهای مجاز",
|
||||
peer: "همتا",
|
||||
peers: "همتاها",
|
||||
},
|
||||
lb: {
|
||||
defaultOut: "خروجی پیشفرض",
|
||||
interruptConn: "قطع ارتباط موجود",
|
||||
testUrl: "URL تست",
|
||||
interval: "فاصله زمانی",
|
||||
tolerance: "تحمل",
|
||||
urlTestOptions: "گزینههای URLTest"
|
||||
},
|
||||
ts: {
|
||||
options: "گزینههای Tailscale",
|
||||
stateDir: "مسیر پوشه وضعیت",
|
||||
authKey: "کلید احراز هویت",
|
||||
relayServer: "سرور رله",
|
||||
relayServerPort: "پورت سرور رله",
|
||||
relayEndpoints: "نقاط انتهایی ثابت رله",
|
||||
systemInterface: "رابط سیستمی",
|
||||
sysIfName: "نام رابط",
|
||||
sysIfMtu: "MTU رابط",
|
||||
controlUrl: "درگاه کنترل",
|
||||
ephemeral: "حالت موقتی",
|
||||
hostname: "نام هاست",
|
||||
acceptRoutes: "پذیرش مسیرها",
|
||||
exitNode: "درگاه خروج",
|
||||
allowLanAccess: "دسترسی به LAN",
|
||||
advRoutes: "تبلیغ مسیرها",
|
||||
advExitNode: "تبلیغ درگاه خروج",
|
||||
udpTimeout: "مهلت UDP",
|
||||
},
|
||||
ocm: {
|
||||
credentialPath: "مسیر اعتبارنامه",
|
||||
usagesPath: "مسیر آمار استفاده",
|
||||
users: "کاربران",
|
||||
userName: "نام",
|
||||
userToken: "توکن",
|
||||
},
|
||||
ccm: {
|
||||
credentialPath: "مسیر اعتبارنامه",
|
||||
usagesPath: "مسیر آمار استفاده",
|
||||
users: "کاربران",
|
||||
userName: "نام",
|
||||
userToken: "توکن",
|
||||
},
|
||||
derp: {
|
||||
configPath: "مسیر پیکربندی",
|
||||
verifyClientEndpoint: "درگاه تایید کلاینت",
|
||||
verifyClientUrl: "URL تایید کلاینت",
|
||||
meshWith: "شبکه مش",
|
||||
meshPsk: "کلید پیشاشتراکگذاری",
|
||||
meshPskFile: "فایل کلید پیشاشتراکگذاری",
|
||||
stun: "سرور STUN",
|
||||
options: "گزینههای DERP",
|
||||
},
|
||||
naive: {
|
||||
insecureConcurrency: "همزمانی ناامن",
|
||||
quic: "QUIC",
|
||||
quicCongestion: "کنترل تراکم QUIC",
|
||||
udpOverTcp: "UDP روی TCP",
|
||||
},
|
||||
anytls: {
|
||||
idleInterval: "فاصله بررسی جلسه غیرفعال",
|
||||
idleTimeout: "زمان پایان جلسه غیرفعال",
|
||||
minIdle: "حداقل جلسات غیرفعال",
|
||||
},
|
||||
},
|
||||
in: {
|
||||
addr: "آدرس",
|
||||
port: "پورت",
|
||||
ssMethod: "روش",
|
||||
ssManageable: "قابل مدیریت",
|
||||
sSide: "سمت سرور",
|
||||
cSide: "سمت کاربر",
|
||||
multiDomain: "دامنه چندگانه",
|
||||
remark: "شرح",
|
||||
mdOption: "گزینههای دامنه چندگانه",
|
||||
},
|
||||
listen: {
|
||||
options: "گزینههای گوشدادن",
|
||||
tcpOptions: "گزینههای TCP",
|
||||
udpOptions: "گزینههای UDP",
|
||||
detour: "انحراف مسیر",
|
||||
detourText: "ارسال به ورودی دیگر",
|
||||
disableTcpKeepAlive: "غیرفعالسازی TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "فاصله TCP Keep Alive",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "اتصال به کارت شبکه",
|
||||
bindIp4: "اتصال به IPv4",
|
||||
bindIp6: "اتصال به IPv6",
|
||||
bindNoPort: "اتصال آدرس بدون پورت",
|
||||
reuseAddr: "استفاده مجدد از آدرس",
|
||||
connTimeout: "مهلت ارتباط",
|
||||
disableTcpKeepAlive: "غیرفعالسازی TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "فاصله TCP Keep Alive",
|
||||
domainResolver: "حلکننده دامنه",
|
||||
options: "گزینههای تماس",
|
||||
detourText: "ارسال به خروجی دیگر",
|
||||
},
|
||||
transport: {
|
||||
enable: "فعالسازی انتقال",
|
||||
host: "دامنه",
|
||||
hosts: "دامنهها",
|
||||
path: "مسیر",
|
||||
httpMethod: "متد درخواست",
|
||||
idleTimeout: "مهلت بیکاری",
|
||||
pingTimeout: "مهلت پینگ",
|
||||
grpcServiceName: "نام سرویس",
|
||||
grpcPws: "حفظ ارتباط بدون دیتا",
|
||||
},
|
||||
mux: {
|
||||
enable: "فعالسازی تسهیم",
|
||||
maxConn: "بیشینه ارتباطات",
|
||||
minStr: "کمینه استریم",
|
||||
maxStr: "بیشینه استریم",
|
||||
padding: "فقط با پدینگ",
|
||||
enableBrutal: "فعالسازی شدت",
|
||||
},
|
||||
out: {
|
||||
addr: "آدرس سرور",
|
||||
port: "پورت سرور",
|
||||
addUrlTest: "افزودن URLTest",
|
||||
delay: "تأخیر"
|
||||
},
|
||||
rule: {
|
||||
add: "ایجاد قانون",
|
||||
simple: "ساده",
|
||||
logical: "منطقی",
|
||||
mode: "حالت",
|
||||
invert: "برعکس",
|
||||
ipVer: "نسخه IP",
|
||||
domain: "دامنهها",
|
||||
domainSufix: "پسوندهای دامنه",
|
||||
domainKw: "کلمات کلیدی دامنه",
|
||||
domainRgx: "رجکس دامنه",
|
||||
ip: "محدودههای IP",
|
||||
privateIp: "آدرس های IP نامعتبر",
|
||||
port: "پورتها",
|
||||
portRange: "محدودههای پورت",
|
||||
srcCidr: "محدودههای آدرس IP مبدا",
|
||||
srcPrivateIp: "آدرسهای IP مبدا نامعتبر",
|
||||
srcPort: "پورتهای مبدا",
|
||||
srcPortRange: "محدوده پورتهای منبع",
|
||||
ruleset: "مجموعهها",
|
||||
rulesetMatchSrc: "تطابق آدرسهای مبدا با مجموعه قوانین",
|
||||
preferredBy: "ترجیح داده شده توسط",
|
||||
interfaceAddr: "آدرس رابط شبکه",
|
||||
options: "گزینههای قوانین",
|
||||
domainRules: "دامنه/آدرس",
|
||||
srcIpRules: "آدرس مبدا",
|
||||
srcPortRules: "پورت مبدا",
|
||||
udpDisableDomainUnmapping: "عدم تبدیل مسیریابی دامنه",
|
||||
udpConnect: "اتصال UDP",
|
||||
udpTimeout: "مهلت UDP",
|
||||
method: "روش",
|
||||
noDrop: "عدم رهاکردن",
|
||||
sniffer: "شنود کننده",
|
||||
timeout: "مهلت",
|
||||
strategy: "استراتژی",
|
||||
etaHint: "یک آیتم برای هر خط وارد کنید. خطوط خالی و تکراری حذف خواهند شد.",
|
||||
import: {
|
||||
title: "ورود دستهای مجموعههای قانون",
|
||||
rulesTitle: "ورود قوانین",
|
||||
urlsHint: "در هر خط یک URL. برچسب از نام فایل بدون پسوند تعیین میشود.",
|
||||
fileHint: "فایل .txt حاوی URLها را بارگذاری کنید؛ در هر خط یک مورد.",
|
||||
jsonHint: "یک شی JSON با آرایههای rules و/یا rule_set قرار دهید. میتوانید کل بلوک «route»: {'{'}...{'}'} یا فقط محتوایش را بچسبانید.",
|
||||
fileJsonHint: "فایل .json حاوی بلوک route را بارگذاری کنید.",
|
||||
urlHint: "لینک مستقیم فایل JSON را وارد کنید (مثلاً لینک raw گیتهاب).",
|
||||
preview: "پیشنمایش",
|
||||
skipped: "از قبل وجود دارند، بهصورت خاکستری نمایش داده شدهاند",
|
||||
conflict: "تعارض شناسایی شد",
|
||||
merge: "ادغام — افزودن قوانین واردشده (رد برچسب تکراری مجموعهها)",
|
||||
replace: "جایگزینی — حذف قوانین و مجموعههای موجود و ورود دوباره",
|
||||
pasteUrls: "چسباندن URL",
|
||||
uploadTxt: "بارگذاری .txt",
|
||||
uploadFile: "بارگذاری فایل",
|
||||
fromUrl: "از طریق لینک",
|
||||
selectTxt: "انتخاب فایل .txt",
|
||||
selectJson: "انتخاب فایل .json",
|
||||
parse: "تجزیه",
|
||||
conflictDesc: "پیکربندی از قبل {rules} قانون و {rulesets} مجموعه قانون دارد. عمل را انتخاب کنید:",
|
||||
finalOutbound: "خروجی پیشفرض (final)",
|
||||
applyFinal: "اعمال بهعنوان خروجی پیشفرض",
|
||||
errNoArrays: 'هیچ آرایهٔ «rules» یا «rule_set» یافت نشد.',
|
||||
errJsonParse: "خطای تجزیهٔ JSON: <p dir='ltr'>{message}</p>",
|
||||
errNoArraysFetched: 'در JSON دریافتشده «rules» یا «rule_set» یافت نشد.',
|
||||
errFetch: "خطای دریافت: <p dir='ltr'>{message}</p>",
|
||||
errNoFile: "فایلی انتخاب نشده است.",
|
||||
errNoArraysInFile: 'در فایل «rules» یا «rule_set» یافت نشد.',
|
||||
},
|
||||
},
|
||||
ruleset: {
|
||||
add: "ایجاد مجموعه",
|
||||
format: "فرمت دادهها",
|
||||
interval: "بازه بروزرسانیها",
|
||||
remote: "راه دور",
|
||||
local: "محلی",
|
||||
},
|
||||
dns: {
|
||||
add: "ایجاد سرور DNS",
|
||||
title: "سرورهای DNS",
|
||||
final: "سرور نهایی",
|
||||
server: "سرور",
|
||||
firstServer: "سرور نخست",
|
||||
cacheCapacity: "ظرفیت cache",
|
||||
disableCache: "غیرفعالسازی cache",
|
||||
disableExpire: "بدون انقضا",
|
||||
independentCache: "استقلال cache",
|
||||
reverseMapping: "نگاشت معکوس",
|
||||
domainStrategy: "استراتژی دامنه",
|
||||
local: { preferGo: "ترجیح Go" },
|
||||
rule: {
|
||||
add: "ایجاد قانون DNS",
|
||||
title: "قوانین DNS",
|
||||
inet4Range: "محدوده IPv4",
|
||||
inet6Range: "محدوده IPv6",
|
||||
acceptDefault: "پذیرش پیشفرض",
|
||||
action: {
|
||||
title: "عملیات",
|
||||
route: "مسیریابی",
|
||||
routeOptions: "گزینههای مسیریابی",
|
||||
reject: "رد کردن",
|
||||
predefined: "پیش تعریف شده",
|
||||
rewriteTtl: "بازنویسی TTL",
|
||||
clientSubnet: "زیر شبکه کاربر",
|
||||
rcode: "کد جواب",
|
||||
rcodes: {
|
||||
noError: "درست",
|
||||
formerr: "درخواست نامعتبر",
|
||||
servFail: "خطای سرور",
|
||||
nxDomain: "یافت نشد",
|
||||
refused: "رد شده",
|
||||
notImp: "پیادهسازی نشده",
|
||||
},
|
||||
answer: "پاسخها",
|
||||
ns: "سرورهای دامنه",
|
||||
extra: "اضافی",
|
||||
},
|
||||
},
|
||||
},
|
||||
basic: {
|
||||
log: {
|
||||
title: "گزارشها",
|
||||
level: "سطح",
|
||||
output: "خروجی",
|
||||
timestamp: "فعالسازی ثبت زمان",
|
||||
},
|
||||
routing: {
|
||||
title: "مسیریابی",
|
||||
defaultOut: "خروجی پیشفرض",
|
||||
defaultIf: "کارت شبکه پیشفرض",
|
||||
defaultRm: "Routing Mark پیشفرض",
|
||||
defaultDns: "DNS پیشفرض",
|
||||
autoBind: "انتخاب اتوماتیک کارت شبکه",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "ذخیره آدرسهای نامعتبر",
|
||||
extController: "کنترلگر خارجی",
|
||||
extUi: "رابطکاربری خارجی",
|
||||
extUiDownloadUrl: "آدرس دانلود رابطکاربری",
|
||||
extUiDownloadDetour: "خروجی دانلود رابطکاربری",
|
||||
secret: "رمز",
|
||||
defaultMode: "حالت پیشفرض",
|
||||
allowOrigin: "اجازه از مبدا",
|
||||
allowPrivate: "اجازه شبکه خصوصی",
|
||||
},
|
||||
},
|
||||
tls: {
|
||||
enable: "فعالسازی رمزنگاری",
|
||||
usePath: "مسیر فایل",
|
||||
useText: "متن گواهی",
|
||||
certPath: "مسیر فایل گواهی",
|
||||
keyPath: "مسیر فایل کلید",
|
||||
cert: "گواهی",
|
||||
key: "کلید",
|
||||
options: "گزینههای رمزنگاری",
|
||||
minVer: "کمینه نسخه",
|
||||
maxVer: "بیشینه نسخه",
|
||||
cs: "مدلهای رمزنگاری",
|
||||
privKey: "کلید خصوصی",
|
||||
pubKey: "کلید عمومی",
|
||||
disableSni: "غیرفعالسازی SNI",
|
||||
insecure: "تایید ارتباط ناامن",
|
||||
fragment: "تکهبندی",
|
||||
fragmentDelay: "تاخیر تکهبندی جایگزین",
|
||||
recordFragment: "تکهبندی چندگانه",
|
||||
store: "انبار ریشه",
|
||||
ktls: "TLS هسته",
|
||||
kernelTx: "ارسال",
|
||||
kernelRx: "دریافت",
|
||||
queryServerName: "نام سرور ECH برای جستجو",
|
||||
acme: {
|
||||
options: "گزینههای ACME",
|
||||
dataDir: "مسیر دادهها",
|
||||
defaultDomain: "دامنه پیشفرض",
|
||||
disableChallenges: "بستن چالشها",
|
||||
httpChallenge: "بستن چالش HTTP",
|
||||
tlsChallenge: "بستن چالش TLS",
|
||||
altPorts: "پورتهای جایگزین",
|
||||
altHport: "پورت جایگزین HTTP",
|
||||
altTport: "پورت جایگزین TLS",
|
||||
caProvider: "فراهم کننده گواهی",
|
||||
customCa: "فراهم کننده دیگر",
|
||||
extAcc: "حساب خارجی",
|
||||
dns01: "چالش DNS01",
|
||||
dns01Provider: "فراهم کننده چالش DNS01",
|
||||
dns01Params: {
|
||||
api_token: "توکن API",
|
||||
zone_token: "توکن زون",
|
||||
access_key_id: "شناسه کلید دسترسی",
|
||||
access_key_secret: "رمز کلید دسترسی",
|
||||
region_id: "شناسه منطقه",
|
||||
security_token: "توکن امنیتی",
|
||||
username: "نام کاربری",
|
||||
password: "رمز عبور",
|
||||
subdomain: "زیردامنه",
|
||||
server_url: "آدرس سرور",
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "آپلود",
|
||||
download: "دانلود",
|
||||
volume: "حجم",
|
||||
usage: "استفاده",
|
||||
enable: "فعال سازی گزارش ترافیک",
|
||||
graphTitle: "نمودار ترافیک",
|
||||
B: "ب",
|
||||
KB: "کب",
|
||||
MB: "مب",
|
||||
GB: "گب",
|
||||
TB: "تب",
|
||||
PB: "پب",
|
||||
p: "پ",
|
||||
Kp: "کپ",
|
||||
Mp: "مپ",
|
||||
Gp: "گپ",
|
||||
Mbps: "مب/ث",
|
||||
},
|
||||
date: {
|
||||
expiry: "انقضا",
|
||||
expired: "منقضی",
|
||||
d: "ر",
|
||||
h: "س",
|
||||
m: "د",
|
||||
s: "ث",
|
||||
ms: "مث",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './en'
|
||||
import fa from './fa'
|
||||
import vi from './vi'
|
||||
import zhcn from './zhcn'
|
||||
import zhtw from './zhtw'
|
||||
import ru from './ru'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: localStorage.getItem("locale") ?? 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en: en,
|
||||
fa: fa,
|
||||
vi: vi,
|
||||
zhHans: zhcn,
|
||||
zhHant: zhtw,
|
||||
ru: ru
|
||||
},
|
||||
})
|
||||
|
||||
export const locale = (() => {
|
||||
const l = i18n.global.locale.value
|
||||
switch (l) {
|
||||
case "zhHans":
|
||||
return "zh-cn"
|
||||
case "zhHant":
|
||||
return "zh-tw"
|
||||
default:
|
||||
return l
|
||||
}
|
||||
})()
|
||||
|
||||
export const languages = [
|
||||
{ title: 'English', value: 'en' },
|
||||
{ title: 'فارسی', value: 'fa' },
|
||||
{ title: 'Tiếng Việt', value: 'vi' },
|
||||
{ title: '简体中文', value: 'zhHans' },
|
||||
{ title: '繁體中文', value: 'zhHant' },
|
||||
{ title: 'Русский', value: 'ru' },
|
||||
]
|
||||
@@ -0,0 +1,640 @@
|
||||
export default {
|
||||
success: "успех",
|
||||
failed: "ошибка",
|
||||
enable: "Включить",
|
||||
disable: "Отключить",
|
||||
none: "Никакие",
|
||||
all: "Все",
|
||||
loading: "Загрузка...",
|
||||
confirm: "Вы уверены?",
|
||||
yes: "да",
|
||||
no: "нет",
|
||||
unlimited: "бесконечный",
|
||||
type: "Тип",
|
||||
protocol: "Протокол",
|
||||
submit: "Отправить",
|
||||
reset: "Сбросить",
|
||||
now: "Сейчас",
|
||||
network: "Сеть",
|
||||
copyToClipboard: "Копировать в буфер обмена",
|
||||
noData: "Нет данных!",
|
||||
invalidLogin: "Неверный логин!",
|
||||
online: "В сети",
|
||||
version: "Версия",
|
||||
email: "Электронная почта",
|
||||
commaSeparated: "(разделено запятыми)",
|
||||
count: "Количество",
|
||||
template: "Шаблон",
|
||||
editor: "Редактор",
|
||||
error: {
|
||||
dplData: "Дублирующие данные",
|
||||
core: "Ошибка Sing-Box",
|
||||
invalidData: "Неверные данные",
|
||||
},
|
||||
theme: {
|
||||
light: "Светлый",
|
||||
dark: "Темный",
|
||||
system: "Система",
|
||||
},
|
||||
pages: {
|
||||
login: "Вход",
|
||||
home: "Главная",
|
||||
inbounds: "Входящие",
|
||||
outbounds: "Исходящие",
|
||||
services: "Устройства",
|
||||
endpoints: "Эндпоинты",
|
||||
clients: "Клиенты",
|
||||
rules: "Правила",
|
||||
tls: "Настройки TLS",
|
||||
basics: "Основы",
|
||||
dns: "DNS",
|
||||
admins: "Администраторы",
|
||||
settings: "Настройки",
|
||||
},
|
||||
main: {
|
||||
tiles: "Плитки",
|
||||
gauges: "Датчики",
|
||||
charts: "Графики",
|
||||
infos: "Информация",
|
||||
gauge: {
|
||||
cpu: "Загрузка ЦП",
|
||||
mem: "Загрузка ОЗУ",
|
||||
dsk: "Загрузка диска",
|
||||
swp: "Загрузка Swap",
|
||||
},
|
||||
chart: {
|
||||
cpu: "Мониторинг ЦП",
|
||||
mem: "Мониторинг ОЗУ",
|
||||
net: "Сетевой трафик",
|
||||
pnet: "Сетевые пакеты",
|
||||
dio: "Мониторинг диска",
|
||||
},
|
||||
info: {
|
||||
sys: "Информация о системе",
|
||||
sbd: "Информация о Sing-Box",
|
||||
host: "Хост",
|
||||
cpu: "ЦП",
|
||||
core: "Ядро",
|
||||
uptime: "Время работы",
|
||||
startupTime: "Время запуска",
|
||||
threads: "Потоки",
|
||||
memory: "Память",
|
||||
running: "Работает"
|
||||
},
|
||||
backup: {
|
||||
title: "Резервное копирование и восстановление",
|
||||
backup: "Скачать резервную копию",
|
||||
restore: "Восстановить резервную копию",
|
||||
exclStats: "Исключить графики",
|
||||
exclChanges: "Исключить изменения",
|
||||
sbConfig: "Скачать конфигурацию Sing-Box",
|
||||
},
|
||||
stats: {
|
||||
title: "Использование и количество",
|
||||
totalUsage: "Общее использование",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "Входящий",
|
||||
client: "Клиент",
|
||||
outbound: "Исходящий",
|
||||
endpoint: "Точка входа",
|
||||
config: "Настройки",
|
||||
rule: "Правило",
|
||||
ruleset: "Набор правил",
|
||||
service: "Устройство",
|
||||
dnsserver: "DNS сервер",
|
||||
dnsrule: "Правило DNS",
|
||||
user: "Пользователь",
|
||||
tag: "Тег",
|
||||
listen: "Прослушивание",
|
||||
dial: "Звонок",
|
||||
tls: "TLS",
|
||||
multiplex: "Мультиплекс",
|
||||
transport: "Транспорт",
|
||||
headers: "Заголовки",
|
||||
key: "Ключ",
|
||||
value: "Значение",
|
||||
},
|
||||
actions: {
|
||||
action: "Действие",
|
||||
add: "Добавить",
|
||||
addbulk: "Добавить пакетно",
|
||||
editbulk: "Редактировать пакетно",
|
||||
delbulk: "Удалить пакетно",
|
||||
new: "Новый",
|
||||
edit: "Редактировать",
|
||||
del: "Удалить",
|
||||
clone: "Клонировать",
|
||||
test: "Тест",
|
||||
testAll: "Тестировать все",
|
||||
save: "Сохранить",
|
||||
update: "Обновить",
|
||||
submit: "Отправить",
|
||||
set: "Установить",
|
||||
generate: "Генерировать",
|
||||
disable: "Отключить",
|
||||
close: "Закрыть",
|
||||
restartApp: "Перезапустить приложение",
|
||||
restartSb: "Перезапустить Singbox",
|
||||
apply: "Применить",
|
||||
},
|
||||
login: {
|
||||
title: "Вход",
|
||||
username: "Имя пользователя",
|
||||
unRules: "Имя пользователя не может быть пустым",
|
||||
password: "Пароль",
|
||||
pwRules: "Пароль не может быть пустым",
|
||||
},
|
||||
menu: {
|
||||
logout: "Выйти",
|
||||
},
|
||||
admin: {
|
||||
changeCred: "Изменить учетные данные",
|
||||
oldPass: "Текущий пароль",
|
||||
newUname: "Новое имя пользователя",
|
||||
newPass: "Новый пароль",
|
||||
lastLogin: "Последний вход",
|
||||
date: "Дата",
|
||||
time: "Время",
|
||||
changes: "Изменения",
|
||||
actor: "Исполнитель",
|
||||
key: "Ключ",
|
||||
action: "Действие",
|
||||
api: {
|
||||
title: "Токены API",
|
||||
msg: "Пожалуйста, скопируйте токен ниже и сохраните его в безопасном месте. Он не будет показан заново.",
|
||||
token: "Токен",
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
interface: "Интерфейс",
|
||||
sub: "Подписка",
|
||||
addr: "Адрес",
|
||||
port: "Порт",
|
||||
webPath: "Базовый URI",
|
||||
domain: "Домен",
|
||||
sslKey: "Путь к SSL ключу",
|
||||
sslCert: "Путь к SSL сертификату",
|
||||
webUri: "URI панели",
|
||||
sessionAge: "Максимальная длительность сессии",
|
||||
trafficAge: "Максимальная длительность трафика",
|
||||
timeLoc: "Часовой пояс",
|
||||
subEncode: "Включить кодирование",
|
||||
subInfo: "Включить информацию о клиенте",
|
||||
path: "Путь по умолчанию",
|
||||
update: "Время автоматического обновления",
|
||||
subUri: "URI подписки",
|
||||
jsonSub: "JSON подписка",
|
||||
toDirect: "Маршрутизация на Direct",
|
||||
toBlock: "Маршрутизация на Block",
|
||||
timestamp: "Метка времени",
|
||||
globalDns: "Глобальный DNS",
|
||||
directDns: "Прямой DNS",
|
||||
toDirectDns: "Маршрутизация на Direct DNS",
|
||||
jsonSubOptions: "Другие параметры",
|
||||
excludePkg: "Исключить пакеты",
|
||||
clashSub: "Clash подписка",
|
||||
mixedPort: "Смешанный порт",
|
||||
tun: "Tun инбоунд",
|
||||
},
|
||||
client: {
|
||||
name: "Имя",
|
||||
desc: "Описание",
|
||||
group: "Группа",
|
||||
inboundTags: "Теги входящих",
|
||||
basics: "Основы",
|
||||
config: "Конфигурация",
|
||||
links: "Ссылки",
|
||||
external: "Внешняя ссылка",
|
||||
sub: "Внешняя подписка",
|
||||
delayStart: "Отложенный старт",
|
||||
autoReset: "Авто сброс",
|
||||
resetDays: "Дней до сброса",
|
||||
nextReset: "Следующий сброс",
|
||||
},
|
||||
bulk: {
|
||||
order: "Порядок",
|
||||
random: "Случайный",
|
||||
changeLimits: "Изменить лимиты",
|
||||
addInbounds: "Добавить входящие",
|
||||
removeInbounds: "Удалить входящие",
|
||||
addDays: "Добавить дни",
|
||||
addVolume: "Добавить объём",
|
||||
},
|
||||
types: {
|
||||
un: "Имя пользователя",
|
||||
pw: "Пароль",
|
||||
direct: {
|
||||
overrideAddr: "Переопределить адрес",
|
||||
overridePort: "Переопределить порт",
|
||||
},
|
||||
hy: {
|
||||
obfs: "Обфусцированный пароль",
|
||||
auth: "Пароль аутентификации",
|
||||
hyOptions: "Параметры Hysteria",
|
||||
hy2Options: "Параметры Hysteria2",
|
||||
ignoreBw: "Игнорировать пропускную способность клиента",
|
||||
},
|
||||
shdwTls: {
|
||||
hs: "Сервер рукопожатий",
|
||||
addHS: "Добавить сервер рукопожатий",
|
||||
},
|
||||
ssh: {
|
||||
passphrase: "Парольная фраза",
|
||||
hostKey: "Ключи хоста",
|
||||
algorithm: "Алгоритмы ключей",
|
||||
clientVer: "Версия клиента",
|
||||
options: "Параметры SSH",
|
||||
},
|
||||
tor: {
|
||||
execPath: "Путь к исполняемому файлу",
|
||||
dataDir: "Каталог данных",
|
||||
extArgs: "Дополнительные аргументы",
|
||||
},
|
||||
tuic: {
|
||||
congControl: "Контроль перегрузок",
|
||||
authTimeout: "Таймаут аутентификации",
|
||||
hb: "Сердцебиение",
|
||||
},
|
||||
tun: {
|
||||
addr: "Адреса",
|
||||
ifName: "Имя интерфейса",
|
||||
excludeMptcp: "Исключить MPTCP",
|
||||
fallbackRuleIndex: "Индекс правила iproute2 fallback",
|
||||
},
|
||||
vless: {
|
||||
flow: "Поток",
|
||||
udpEnc: "Кодирование UDP пакетов",
|
||||
},
|
||||
vmess: {
|
||||
security: "Безопасность",
|
||||
globalPadding: "Глобальная подкладка",
|
||||
authLen: "Длина шифрования",
|
||||
},
|
||||
wg: {
|
||||
privKey: "Приватный ключ",
|
||||
pubKey: "Публичный ключ пира",
|
||||
psk: "Предварительно разделенный ключ",
|
||||
localIp: "Локальные IP",
|
||||
worker: "Работники",
|
||||
ifName: "Имя интерфейса",
|
||||
sysIf: "Системный интерфейс",
|
||||
options: "Параметры Wireguard",
|
||||
allowedIp: "Разрешенные IP",
|
||||
peer: "Пир",
|
||||
peers: "Пиры",
|
||||
},
|
||||
lb: {
|
||||
defaultOut: "Исходящий по умолчанию",
|
||||
interruptConn: "Прервать существующие соединения",
|
||||
testUrl: "Тестовый URL",
|
||||
interval: "Интервал",
|
||||
tolerance: "Толерантность",
|
||||
urlTestOptions: "Параметры URLTest"
|
||||
},
|
||||
ts: {
|
||||
options: "Параметры Tailscale",
|
||||
stateDir: "Каталог состояния",
|
||||
authKey: "Ключ аутентификации",
|
||||
relayServer: "Сервер ретрансляции",
|
||||
relayServerPort: "Порт сервера ретрансляции",
|
||||
relayEndpoints: "Статические точки ретрансляции",
|
||||
systemInterface: "Системный интерфейс",
|
||||
sysIfName: "Имя интерфейса",
|
||||
sysIfMtu: "MTU интерфейса",
|
||||
controlUrl: "URL управления",
|
||||
ephemeral: "Эфемерный",
|
||||
hostname: "Имя хоста",
|
||||
acceptRoutes: "Принять маршруты",
|
||||
exitNode: "Выходной узел",
|
||||
allowLanAccess: "Разрешить доступ LAN",
|
||||
advRoutes: "Рекламируемые маршруты",
|
||||
advExitNode: "Рекламируемый выходной узел",
|
||||
udpTimeout: "Таймаут UDP",
|
||||
},
|
||||
ocm: {
|
||||
credentialPath: "Путь к учетным данным",
|
||||
usagesPath: "Путь к статистике",
|
||||
users: "Пользователи",
|
||||
userName: "Имя",
|
||||
userToken: "Токен",
|
||||
},
|
||||
ccm: {
|
||||
credentialPath: "Путь к учетным данным",
|
||||
usagesPath: "Путь к статистике",
|
||||
users: "Пользователи",
|
||||
userName: "Имя",
|
||||
userToken: "Токен",
|
||||
},
|
||||
derp: {
|
||||
configPath: "Путь к конфигурации",
|
||||
verifyClientEndpoint: "Проверить конечную точку клиента",
|
||||
verifyClientUrl: "Проверить URL клиента",
|
||||
meshWith: "Сеть с",
|
||||
meshPsk: "Предварительно разделенный ключ",
|
||||
meshPskFile: "Файл предварительно разделенного ключа",
|
||||
stun: "Сервер STUN",
|
||||
options: "Параметры DERP",
|
||||
},
|
||||
naive: {
|
||||
insecureConcurrency: "Небезопасная параллельность",
|
||||
quic: "QUIC",
|
||||
quicCongestion: "Управление перегрузкой QUIC",
|
||||
udpOverTcp: "UDP через TCP",
|
||||
},
|
||||
anytls: {
|
||||
idleInterval: "Интервал проверки неактивных сессий",
|
||||
idleTimeout: "Тайм-аут неактивной сессии",
|
||||
minIdle: "Минимум неактивных сессий"
|
||||
},
|
||||
},
|
||||
in: {
|
||||
addr: "Адрес",
|
||||
port: "Порт",
|
||||
ssMethod: "Метод",
|
||||
ssManageable: "Управляемый",
|
||||
sSide: "Сторона сервера",
|
||||
cSide: "Сторона клиента",
|
||||
multiDomain: "Мультидомен",
|
||||
remark: "Примечание",
|
||||
mdOption: "Параметры мультидомена",
|
||||
},
|
||||
listen: {
|
||||
options: "Параметры прослушивания",
|
||||
tcpOptions: "Параметры TCP",
|
||||
udpOptions: "Параметры UDP",
|
||||
detour: "Обход",
|
||||
detourText: "Переадресация на входящий",
|
||||
disableTcpKeepAlive: "Отключить TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "Интервал TCP Keep Alive",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "Привязка к сетевому интерфейсу",
|
||||
bindIp4: "Привязка к IPv4",
|
||||
bindIp6: "Привязка к IPv6",
|
||||
bindNoPort: "Привязка адреса без порта",
|
||||
reuseAddr: "Повторное использование адреса слушателя",
|
||||
connTimeout: "Таймаут подключения",
|
||||
disableTcpKeepAlive: "Отключить TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "Интервал TCP Keep Alive",
|
||||
domainResolver: "Разрешение домена",
|
||||
options: "Параметры вызова",
|
||||
detourText: "Переадресация на исходящий",
|
||||
},
|
||||
transport: {
|
||||
enable: "Включить транспорт",
|
||||
host: "Хост",
|
||||
hosts: "Хосты",
|
||||
path: "Путь",
|
||||
httpMethod: "Метод запроса",
|
||||
idleTimeout: "Таймаут бездействия",
|
||||
pingTimeout: "Таймаут пинга",
|
||||
grpcServiceName: "Имя службы",
|
||||
grpcPws: "Разрешить без потока",
|
||||
},
|
||||
mux: {
|
||||
enable: "Включить мультиплекс",
|
||||
maxConn: "Максимальное количество соединений",
|
||||
minStr: "Минимальное количество потоков",
|
||||
maxStr: "Максимальное количество потоков",
|
||||
padding: "Только подкладка",
|
||||
enableBrutal: "Включить Brutal",
|
||||
},
|
||||
out: {
|
||||
addr: "Адрес сервера",
|
||||
port: "Порт сервера",
|
||||
addUrlTest: "Добавить URLTest",
|
||||
delay: "Задержка",
|
||||
},
|
||||
rule: {
|
||||
add: "Добавить правило",
|
||||
simple: "Простое",
|
||||
logical: "Логическое",
|
||||
mode: "Режим",
|
||||
invert: "Инвертировать",
|
||||
ipVer: "Версия IP",
|
||||
domain: "Домены",
|
||||
domainSufix: "Суффиксы доменов",
|
||||
domainKw: "Ключевые слова домена",
|
||||
domainRgx: "Регулярные выражения домена",
|
||||
ip: "CIDR IP",
|
||||
privateIp: "Недействительные диапазоны IP",
|
||||
port: "Порты",
|
||||
portRange: "Диапазоны портов",
|
||||
srcCidr: "CIDR исходного IP",
|
||||
srcPrivateIp: "Недействительные исходные IP",
|
||||
srcPort: "Исходные порты",
|
||||
srcPortRange: "Диапазоны исходных портов",
|
||||
ruleset: "Наборы правил",
|
||||
rulesetMatchSrc: "Набор правил для соответствия источника IPcidr",
|
||||
preferredBy: "Предпочтительный исходящий",
|
||||
interfaceAddr: "Адрес интерфейса",
|
||||
options: "Параметры правила",
|
||||
domainRules: "Домен/IP",
|
||||
srcIpRules: "Источник IP",
|
||||
srcPortRules: "Источник порта",
|
||||
udpDisableDomainUnmapping: "Отключить перенос доменных имен",
|
||||
udpConnect: "Подключение UDP",
|
||||
udpTimeout: "Таймаут UDP",
|
||||
method: "Метод",
|
||||
noDrop: "Не сбрасывать",
|
||||
sniffer: "Обнаружение",
|
||||
timeout: "Таймаут",
|
||||
strategy: "Стратегия",
|
||||
etaHint: "По одной записи в строке. Пустые строки и дубликаты игнорируются.",
|
||||
import: {
|
||||
title: "Массовый импорт наборов правил",
|
||||
rulesTitle: "Импорт правил",
|
||||
urlsHint: "По одному URL в строке. Тег определяется из имени файла без расширения.",
|
||||
fileHint: "Загрузите .txt файл с URL-адресами, по одному в строке.",
|
||||
jsonHint: "Вставьте JSON-объект с массивами rules и/или rule_set. Можно вставить весь блок \"route\": {'{'}...{'}'} или только его содержимое.",
|
||||
fileJsonHint: "Загрузите .json файл с блоком route.",
|
||||
urlHint: "Укажите прямую ссылку на JSON-файл (например, raw-ссылку GitHub).",
|
||||
preview: "Предпросмотр",
|
||||
skipped: "уже существуют, показаны серым",
|
||||
conflict: "Обнаружены конфликты",
|
||||
merge: "Объединить — добавить импортируемые правила (пропустить дубликаты тегов наборов)",
|
||||
replace: "Заменить — удалить существующие правила и наборы, импортировать заново",
|
||||
pasteUrls: "Вставить URL",
|
||||
uploadTxt: "Загрузить .txt",
|
||||
uploadFile: "Загрузить файл",
|
||||
fromUrl: "По ссылке",
|
||||
selectTxt: "Выберите .txt файл",
|
||||
selectJson: "Выберите .json файл",
|
||||
parse: "Разобрать",
|
||||
conflictDesc: "В конфиге уже есть {rules} правил и {rulesets} наборов правил. Выберите действие:",
|
||||
finalOutbound: "Outbound по умолчанию (final)",
|
||||
applyFinal: "Применить как outbound по умолчанию",
|
||||
errNoArrays: 'Массивы "rules" или "rule_set" не найдены.',
|
||||
errJsonParse: "Ошибка разбора JSON: {message}",
|
||||
errNoArraysFetched: 'В полученном JSON нет "rules" или "rule_set".',
|
||||
errFetch: "Ошибка загрузки: {message}",
|
||||
errNoFile: "Файл не выбран.",
|
||||
errNoArraysInFile: 'В файле нет "rules" или "rule_set".',
|
||||
},
|
||||
},
|
||||
ruleset: {
|
||||
add: "Добавить набор правил",
|
||||
format: "Формат данных",
|
||||
interval: "Интервалы обновления",
|
||||
remote: "Удаленный",
|
||||
local: "Локальный",
|
||||
},
|
||||
dns: {
|
||||
add: "Добавить DNS сервер",
|
||||
title: "DNS серверы",
|
||||
final: "Итоговый",
|
||||
server: "Сервер",
|
||||
firstServer: "Первый сервер",
|
||||
cacheCapacity: "Вместимость кэша",
|
||||
disableCache: "Отключить кэш",
|
||||
disableExpire: "Отключить истечение",
|
||||
independentCache: "Независимый кэш",
|
||||
reverseMapping: "Обратное отображение",
|
||||
domainStrategy: "Стратегия домена",
|
||||
local: { preferGo: "Предпочитать Go" },
|
||||
rule: {
|
||||
add: "Добавить правило DNS",
|
||||
title: "Правила DNS",
|
||||
inet4Range: "Диапазон IPv4",
|
||||
inet6Range: "Диапазон IPv6",
|
||||
acceptDefault: "Принять резолверы по умолчанию",
|
||||
action: {
|
||||
title: "Действие",
|
||||
route: "Маршрутизация",
|
||||
routeOptions: "Параметры маршрутизации",
|
||||
reject: "Отклонить",
|
||||
predefined: "Предопределенные",
|
||||
rewriteTtl: "Перезаписать TTL",
|
||||
clientSubnet: "Подсеть клиента",
|
||||
rcode: "Код ответа",
|
||||
rcodes: {
|
||||
noError: "ОК",
|
||||
formerr: "Неверный запрос",
|
||||
servFail: "Сбой сервера",
|
||||
nxDomain: "Не найдено",
|
||||
refused: "Отклонено",
|
||||
notImp: "Не реализовано"
|
||||
},
|
||||
answer: "Ответ",
|
||||
ns: "Серверы имён",
|
||||
extra: "Дополнительные",
|
||||
},
|
||||
}
|
||||
},
|
||||
basic: {
|
||||
log: {
|
||||
title: "Журналы",
|
||||
level: "Уровень",
|
||||
output: "Вывод",
|
||||
timestamp: "Включить метку времени",
|
||||
},
|
||||
routing: {
|
||||
title: "Маршрутизация",
|
||||
defaultOut: "Исходящий по умолчанию",
|
||||
defaultIf: "Сетевой интерфейс по умолчанию",
|
||||
defaultRm: "Маршрут по умолчанию",
|
||||
defaultDns: "DNS по умолчанию",
|
||||
autoBind: "Автопривязка сетевого интерфейса",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "Хранить поддельный IP",
|
||||
extController: "Внешний контроллер",
|
||||
extUi: "Внешний интерфейс",
|
||||
extUiDownloadUrl: "URL загрузки интерфейса",
|
||||
extUiDownloadDetour: "Обход загрузки интерфейса",
|
||||
secret: "Секрет",
|
||||
defaultMode: "Режим по умолчанию",
|
||||
allowOrigin: "Разрешить источник",
|
||||
allowPrivate: "Разрешить частную сеть"
|
||||
},
|
||||
},
|
||||
tls: {
|
||||
enable: "Включить TLS",
|
||||
usePath: "Использовать путь",
|
||||
useText: "Использовать текст",
|
||||
certPath: "Путь к файлу сертификата",
|
||||
keyPath: "Путь к файлу ключа",
|
||||
cert: "Сертификат",
|
||||
key: "Ключ",
|
||||
options: "Параметры TLS",
|
||||
minVer: "Минимальная версия",
|
||||
maxVer: "Максимальная версия",
|
||||
cs: "Наборы шифров",
|
||||
privKey: "Приватный ключ",
|
||||
pubKey: "Публичный ключ",
|
||||
disableSni: "Отключить SNI",
|
||||
insecure: "Разрешить небезопасное",
|
||||
fragment: "Фрагментация",
|
||||
fragmentDelay: "Задержка фрагментации",
|
||||
recordFragment: "Фрагментация записей",
|
||||
store: "Корневое хранилище",
|
||||
ktls: "Ядро TLS",
|
||||
kernelTx: "TX",
|
||||
kernelRx: "RX",
|
||||
queryServerName: "ECH имя сервера для запроса",
|
||||
acme: {
|
||||
options: "Параметры ACME",
|
||||
dataDir: "Каталог данных",
|
||||
defaultDomain: "Домен по умолчанию",
|
||||
disableChallenges: "Отключить вызовы",
|
||||
httpChallenge: "Отключить HTTP вызов",
|
||||
tlsChallenge: "Отключить TLS вызов",
|
||||
altPorts: "Альтернативные порты",
|
||||
altHport: "Альтернативный HTTP порт",
|
||||
altTport: "Альтернативный TLS порт",
|
||||
caProvider: "Поставщик CA",
|
||||
customCa: "Пользовательский поставщик CA",
|
||||
extAcc: "Внешний аккаунт",
|
||||
dns01: "DNS01 вызов",
|
||||
dns01Provider: "Поставщик DNS01 вызова",
|
||||
dns01Params: {
|
||||
api_token: "API Токен",
|
||||
zone_token: "Токен зоны",
|
||||
access_key_id: "ID ключа доступа",
|
||||
access_key_secret: "Секрет ключа доступа",
|
||||
region_id: "ID региона",
|
||||
security_token: "Токен безопасности",
|
||||
username: "Имя пользователя",
|
||||
password: "Пароль",
|
||||
subdomain: "Поддомен",
|
||||
server_url: "URL сервера",
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "Загрузка",
|
||||
download: "Скачивание",
|
||||
volume: "Объем",
|
||||
usage: "Использование",
|
||||
enable: "Включить статистику",
|
||||
graphTitle: "График трафика",
|
||||
B: "Б",
|
||||
KB: "КБ",
|
||||
MB: "МБ",
|
||||
GB: "ГБ",
|
||||
TB: "ТБ",
|
||||
PB: "ПБ",
|
||||
p: "п",
|
||||
Kp: "Кп",
|
||||
Mp: "Мп",
|
||||
Gp: "Гп",
|
||||
Mbps: "Мб/с",
|
||||
},
|
||||
date: {
|
||||
expiry: "Срок действия",
|
||||
expired: "Истек",
|
||||
d: "д",
|
||||
h: "ч",
|
||||
m: "м",
|
||||
s: "с",
|
||||
ms: "мс",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
export default {
|
||||
success: "Thành công",
|
||||
failed: "Thất bại",
|
||||
enable: "Kích hoạt",
|
||||
disable: "Vô hiệu hóa",
|
||||
none: "Không",
|
||||
all: "Tất cả",
|
||||
loading: "Đang tải...",
|
||||
confirm: "Bạn chắc chắn chứ?",
|
||||
yes: "có",
|
||||
no: "không",
|
||||
unlimited: "vô hạn",
|
||||
type: "Loại",
|
||||
protocol: "Giao thức",
|
||||
submit: "Gửi",
|
||||
reset: "Đặt lại",
|
||||
now: "Hiện tại",
|
||||
network: "Mạng",
|
||||
copyToClipboard: "Sao chép vào clipboard",
|
||||
noData: "Không có dữ liệu!",
|
||||
invalidLogin: "Đăng nhập không hợp lệ!",
|
||||
online: "Trực tuyến",
|
||||
version: "Phiên bản",
|
||||
email: "Email",
|
||||
commaSeparated: "(được phân tách bằng dấu phẩy)",
|
||||
count: "Đếm",
|
||||
template: "Mẫu",
|
||||
editor: "Bản sử dụng",
|
||||
error: {
|
||||
dplData: "Dữ liệu trùng lặp",
|
||||
core: "Lỗi Sing-Box",
|
||||
invalidData: "Dữ liệu khỏ hợp lệ",
|
||||
},
|
||||
theme: {
|
||||
light: "Nhật",
|
||||
dark: "Xanh",
|
||||
system: "Phòng bán",
|
||||
},
|
||||
pages: {
|
||||
login: "Đăng nhập",
|
||||
home: "Trang chủ",
|
||||
inbounds: "Đầu Vào",
|
||||
outbounds: "Đầu ra",
|
||||
services: "Thiết bị",
|
||||
endpoints: "Câu hình",
|
||||
clients: "Khách hàng",
|
||||
rules: "Quy tắc",
|
||||
tls: "Cài đặt TLS",
|
||||
basics: "Cơ bản",
|
||||
dns: "DNS",
|
||||
admins: "Quản trị viên",
|
||||
settings: "Cài đặt",
|
||||
},
|
||||
main: {
|
||||
tiles: "OHB",
|
||||
gauges: "Đồng hồ đo",
|
||||
charts: "Biểu đồ",
|
||||
infos: "Thông tin",
|
||||
gauge: {
|
||||
cpu: "Đồng hồ CPU",
|
||||
mem: "Đồng hồ RAM",
|
||||
dsk: "Đồng hồ Disk",
|
||||
swp: "Đồng hồ Swap",
|
||||
},
|
||||
chart: {
|
||||
cpu: "Máy theo dõi CPU",
|
||||
mem: "Máy theo dõi RAM",
|
||||
net: "Băng thông mạng",
|
||||
pnet: "Gói mạng",
|
||||
dio: "Disk I/O",
|
||||
},
|
||||
info: {
|
||||
sys: "Thông tin hệ thống",
|
||||
sbd: "Thông tin Sing-Box",
|
||||
host: "Máy chủ",
|
||||
cpu: "CPU",
|
||||
core: "Nhân",
|
||||
uptime: "Thời gian hoạt động",
|
||||
startupTime: "Thời gian khởi động",
|
||||
threads: "Luồng",
|
||||
memory: "Bộ nhớ",
|
||||
running: "Đang chạy"
|
||||
},
|
||||
backup: {
|
||||
title: "Sao lưu và khôi phục",
|
||||
backup: "Tải xuống bản sao lưu",
|
||||
restore: "Khôi phục bản sao lưu",
|
||||
exclStats: "Loại trừ các biểu đồ",
|
||||
exclChanges: "Loại trừ các thay đổi",
|
||||
sbConfig: "Tải xuống cấu hình Sing-Box",
|
||||
},
|
||||
stats: {
|
||||
title: "Sử dụng và số lượng",
|
||||
totalUsage: "Tổng sử dụng",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "Đầu Vào",
|
||||
client: "Máy Khách hàng",
|
||||
outbound: "Đầu Ra",
|
||||
endpoint: "Điểm cuối",
|
||||
config: "Câu hình",
|
||||
rule: "Quy tắc",
|
||||
ruleset: "Bộ quy tắc",
|
||||
service: "Dịch vụ",
|
||||
dnsserver: "Máy chủ DNS",
|
||||
dnsrule: "Quy tắc DNS",
|
||||
user: "Người dùng",
|
||||
tag: "Thẻ",
|
||||
listen: "Nghe",
|
||||
dial: "Quay số",
|
||||
tls: "TLS",
|
||||
multiplex: "Ghép đa truyền thông ",
|
||||
transport: "Giao thông",
|
||||
headers: "Tiêu đề",
|
||||
key: "Chìa khóa",
|
||||
value: "Giá trị",
|
||||
},
|
||||
actions: {
|
||||
action: "Hành động",
|
||||
add: "Thêm",
|
||||
addbulk: "Thêm Hàng loạt",
|
||||
editbulk: "Chỉnh sửa hàng loạt",
|
||||
delbulk: "Xóa hàng loạt",
|
||||
new: "Mới",
|
||||
edit: "Chỉnh sửa",
|
||||
del: "Xóa",
|
||||
clone: "Nhân bản",
|
||||
test: "Kiểm tra",
|
||||
testAll: "Kiểm tra tất cả",
|
||||
save: "Lưu",
|
||||
update: "Cập nhật",
|
||||
submit: "Gửi",
|
||||
set: "Đặt",
|
||||
generate: "Tạo ra",
|
||||
disable: "Vô hiệu hóa",
|
||||
close: "Đóng",
|
||||
restartApp: "Khởi động lại ứng dụng",
|
||||
restartSb: "Khởi động lại Singbox",
|
||||
},
|
||||
login: {
|
||||
title: "Đăng nhập",
|
||||
username: "Tên người dùng",
|
||||
unRules: "Tên người dùng không thể trống",
|
||||
password: "Mật khẩu",
|
||||
pwRules: "Mật khẩu không thể trống",
|
||||
},
|
||||
menu: {
|
||||
logout: "Đăng xuất",
|
||||
},
|
||||
admin: {
|
||||
changeCred: "Thay đổi thông tin đăng nhập",
|
||||
oldPass: "Mật khẩu hiện tại",
|
||||
newUname: "Tên người dùng mới",
|
||||
newPass: "Mật khẩu mới",
|
||||
lastLogin: "Lân đăng nhập cuôi",
|
||||
date: "Ngày",
|
||||
time: "Thời gian",
|
||||
changes: "Thay đổi",
|
||||
actor: "Diễn viên",
|
||||
key: "Khóa",
|
||||
action: "Hành động",
|
||||
api: {
|
||||
title: "Mã thông báo API",
|
||||
msg: "Vui lòng sao chép mã thông báo bên dưới và lưu trữ nó ở nơi an toàn. Nó sẽ không được hiển thị lại.",
|
||||
token: "Mã thông báo"
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
interface: "Giao diện",
|
||||
sub: "Đăng ký",
|
||||
addr: "Địa chỉ",
|
||||
port: "Cổng",
|
||||
webPath: "Đường dẫn gốc",
|
||||
domain: "Miền",
|
||||
sslKey: "Đường dẫn khóa SSL",
|
||||
sslCert: "Đường dẫn chứng chỉ SSL",
|
||||
webUri: "URI bảng điều khiển",
|
||||
sessionAge: "Tuổi tối đa của phiên",
|
||||
trafficAge: "Tuổi lưu thông tối đa",
|
||||
timeLoc: "Vị trí múi giờ",
|
||||
subEncode: "Kích hoạt mã hóa",
|
||||
subInfo: "Kích hoạt thông tin khách hàng",
|
||||
path: "Đường dẫn mặc định",
|
||||
update: "Thời gian cập nhật tự động",
|
||||
subUri: "URI đăng ký",
|
||||
jsonSub: "Đăng ký JSON",
|
||||
toDirect: "Chuyển hướng tới Trực tiếp",
|
||||
toBlock: "Chuyển hướng tới Chặn",
|
||||
timestamp: "Dấu thời gian",
|
||||
globalDns: "DNS Toàn cầu",
|
||||
directDns: "DNS Trực tiếp",
|
||||
toDirectDns: "Chuyển hướng tới DNS Trực tiếp",
|
||||
jsonSubOptions: "Tùy chọn Khác",
|
||||
excludePkg: "Loại trừ Gói",
|
||||
clashSub: "Clash đăng ký",
|
||||
mixedPort: "Cổng khóa",
|
||||
tun: "Tun đăng ký",
|
||||
},
|
||||
client: {
|
||||
name: "Tên",
|
||||
desc: "Mô tả",
|
||||
group: "Nhóm",
|
||||
inboundTags: "Thẻ đầu vào",
|
||||
basics: "Cơ bản",
|
||||
config: "Cấu hình",
|
||||
links: "Liên kết",
|
||||
external: "Liên kết bên ngoài",
|
||||
sub: "Đăng ký bên ngoài",
|
||||
delayStart: "Trì hoãn bắt đầu",
|
||||
autoReset: "Tự động đặt lại",
|
||||
resetDays: "Số ngày đặt lại",
|
||||
nextReset: "Đặt lại lần sau",
|
||||
},
|
||||
bulk: {
|
||||
order: "Sắp xếp",
|
||||
random: "Ngẫu nhiên",
|
||||
changeLimits: "Thay đổi giới hạn",
|
||||
addInbounds: "Thêm inbound",
|
||||
removeInbounds: "Xóa inbound",
|
||||
addDays: "Thêm ngày",
|
||||
addVolume: "Thêm dung lượng",
|
||||
},
|
||||
types: {
|
||||
un: "Tên người dùng",
|
||||
pw: "Mật khẩu",
|
||||
direct: {
|
||||
overrideAddr: "Ghi đè Địa chỉ",
|
||||
overridePort: "Ghi đè Cổng",
|
||||
},
|
||||
hy: {
|
||||
obfs: "Mật khẩu đã được Ẩn",
|
||||
auth: "Mật khẩu Xác thực",
|
||||
hyOptions: "Tùy chọn Hysteria",
|
||||
hy2Options: "Tùy chọn Hysteria2",
|
||||
ignoreBw: "Bỏ qua Băng thông của Client",
|
||||
},
|
||||
shdwTls: {
|
||||
hs: "Máy chủ Handshake",
|
||||
addHS: "Thêm Máy chủ Handshake",
|
||||
},
|
||||
ssh: {
|
||||
passphrase: "Cụm từ mật khẩu",
|
||||
hostKey: "Khóa Máy chủ",
|
||||
algorithm: "Thuật toán Khóa",
|
||||
clientVer: "Phiên bản Client",
|
||||
options: "Tùy chọn SSH",
|
||||
},
|
||||
tor: {
|
||||
execPath: "Đường dẫn File thực thi",
|
||||
dataDir: "Thư mục Dữ liệu",
|
||||
extArgs: "Đối số Bổ sung",
|
||||
},
|
||||
tuic: {
|
||||
congControl: "Kiểm soát Tắc nghẽn",
|
||||
authTimeout: "Thời gian chờ Xác thực",
|
||||
hb: "Nhịp tim",
|
||||
},
|
||||
tun: {
|
||||
addr: "Địa chỉ",
|
||||
ifName: "Tên Giao diện",
|
||||
excludeMptcp: "Loại trừ MPTCP",
|
||||
fallbackRuleIndex: "Chỉ số quy tắc dự phòng iproute2",
|
||||
},
|
||||
vless: {
|
||||
flow: "Luồng",
|
||||
udpEnc: "Mã hóa Gói UDP",
|
||||
},
|
||||
vmess: {
|
||||
security: "Bảo mật",
|
||||
globalPadding: "Đệm Toàn cầu",
|
||||
authLen: "Chiều dài Mã hóa",
|
||||
},
|
||||
wg: {
|
||||
privKey: "Khóa Riêng tư",
|
||||
pubKey: "Khóa Công khai của Đối tác",
|
||||
psk: "Khóa được Chia sẻ trước",
|
||||
localIp: "IPs Cục bộ",
|
||||
worker: "Công nhân",
|
||||
ifName: "Tên Giao diện",
|
||||
sysIf: "Giao diện Hệ thống",
|
||||
options: "Tùy chọn Wireguard",
|
||||
allowedIp: "IPs được Phép",
|
||||
peer: "Đối tác",
|
||||
peers: "Đối tác",
|
||||
},
|
||||
lb: {
|
||||
defaultOut: "Đầu ra Mặc định",
|
||||
interruptConn: "Ngắt kết nối hiện tại",
|
||||
testUrl: "URL Kiểm tra",
|
||||
interval: "Khoảng thời gian",
|
||||
tolerance: "Sự dung hòa",
|
||||
urlTestOptions: "Tùy chọn URLTest",
|
||||
},
|
||||
ts: {
|
||||
options: "Tùy chọn Tailscale",
|
||||
stateDir: "Thư mục Trạng thái",
|
||||
authKey: "Khóa Xác thực",
|
||||
relayServer: "Máy chủ chuyển tiếp",
|
||||
relayServerPort: "Cổng máy chủ chuyển tiếp",
|
||||
relayEndpoints: "Điểm cuối tĩnh chuyển tiếp",
|
||||
systemInterface: "Giao diện hệ thống",
|
||||
sysIfName: "Tên giao diện",
|
||||
sysIfMtu: "MTU giao diện",
|
||||
controlUrl: "URL Cấu hình",
|
||||
ephemeral: "Tạm thời",
|
||||
hostname: "Tên máy chủ",
|
||||
acceptRoutes: "Chấp nhận Đường dẫn",
|
||||
exitNode: "Nút thoát",
|
||||
allowLanAccess: "Cho phép Truy cập LAN",
|
||||
advRoutes: "Quảng bá Đường dẫn",
|
||||
advExitNode: "Quảng bá Nút thoát",
|
||||
udpTimeout: "Thời gian Chờ UDP",
|
||||
},
|
||||
ocm: {
|
||||
credentialPath: "Đường dẫn Thông tin xác thực",
|
||||
usagesPath: "Đường dẫn Thống kê",
|
||||
users: "Người dùng",
|
||||
userName: "Tên",
|
||||
userToken: "Token",
|
||||
},
|
||||
ccm: {
|
||||
credentialPath: "Đường dẫn Thông tin xác thực",
|
||||
usagesPath: "Đường dẫn Thống kê",
|
||||
users: "Người dùng",
|
||||
userName: "Tên",
|
||||
userToken: "Token",
|
||||
},
|
||||
derp: {
|
||||
configPath: "Đường dẫn Cấu hình",
|
||||
verifyClientEndpoint: "Điểm cuối Xác minh Máy khách",
|
||||
verifyClientUrl: "URL Xác minh Máy khách",
|
||||
meshWith: "Kết nối Mesh với",
|
||||
meshPsk: "Khóa PSK Mesh",
|
||||
meshPskFile: "Tệp Khóa PSK Mesh",
|
||||
stun: "Máy chủ STUN",
|
||||
options: "Tùy chọn DERP",
|
||||
},
|
||||
naive: {
|
||||
insecureConcurrency: "Đồng thời không an toàn",
|
||||
quic: "QUIC",
|
||||
quicCongestion: "Điều khiển tắc nghẽn QUIC",
|
||||
udpOverTcp: "UDP qua TCP",
|
||||
},
|
||||
anytls: {
|
||||
idleInterval: "Khoảng kiểm tra phiên nhàn rỗi",
|
||||
idleTimeout: "Thời gian chờ phiên nhàn rỗi",
|
||||
minIdle: "Số phiên nhàn rỗi tối thiểu"
|
||||
},
|
||||
},
|
||||
in: {
|
||||
addr: "Địa chỉ",
|
||||
port: "Cổng",
|
||||
ssMethod: "Phương thức",
|
||||
ssManageable: "Quản lý được",
|
||||
sSide: "Phía Máy chủ",
|
||||
cSide: "Phía Khách hàng",
|
||||
multiDomain: "Nhiều Tên miền",
|
||||
remark: "Ghi chú",
|
||||
mdOption: "Tùy chọn Nhiều Tên miền",
|
||||
},
|
||||
listen: {
|
||||
options: "Tùy chọn Nghe",
|
||||
tcpOptions: "Tùy chọn TCP",
|
||||
udpOptions: "Tùy chọn UDP",
|
||||
detour: "Lạc đạo",
|
||||
detourText: "Chuyển tiếp tới đầu vào",
|
||||
disableTcpKeepAlive: "Tắt TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "Khoảng thời gian TCP Keep Alive",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "Ràng buộc tới Giao diện Mạng",
|
||||
bindIp4: "Ràng buộc tới IPv4",
|
||||
bindIp6: "Ràng buộc tới IPv6",
|
||||
bindNoPort: "Ràng buộc địa chỉ không cổng",
|
||||
reuseAddr: "Sử dụng lại Địa chỉ Nghe",
|
||||
connTimeout: "Thời gian Chờ Kết nối",
|
||||
disableTcpKeepAlive: "Tắt TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "Khoảng thời gian TCP Keep Alive",
|
||||
domainResolver: "Trình phân giải Tên miền",
|
||||
options: "Tùy chọn Gọi",
|
||||
detourText: "Chuyển tiếp tới thư đi",
|
||||
},
|
||||
transport: {
|
||||
enable: "Kích hoạt vận chuyển",
|
||||
host: "Máy chủ",
|
||||
hosts: "Máy chủ",
|
||||
path: "Đường dẫn",
|
||||
httpMethod: "Phương thức Yêu cầu",
|
||||
idleTimeout: "Thời gian Chờ Chờ đợi",
|
||||
pingTimeout: "Thời gian Chờ Ping",
|
||||
grpcServiceName: "Tên Dịch vụ",
|
||||
grpcPws: "Cho phép mà không Có Luồng",
|
||||
},
|
||||
mux: {
|
||||
enable: "Bật Multiplex",
|
||||
maxConn: "Số kết nối Tối đa",
|
||||
minStr: "Số Luồng Tối thiểu",
|
||||
maxStr: "Số Luồng Tối đa",
|
||||
padding: "Chỉ đệm",
|
||||
enableBrutal: "Bật Brutal",
|
||||
},
|
||||
out: {
|
||||
addr: "Địa chỉ Máy chủ",
|
||||
port: "Cổng Máy chủ",
|
||||
addUrlTest: "Thêm URLTest",
|
||||
delay: "Độ trễ",
|
||||
},
|
||||
rule: {
|
||||
add: "Thêm Quy tắc",
|
||||
simple: "Đơn giản",
|
||||
logical: "Logic",
|
||||
mode: "Chế độ",
|
||||
invert: "Nghịch đảo",
|
||||
ipVer: "Phiên bản IP",
|
||||
domain: "Tên miền",
|
||||
domainSufix: "Hậu tố Miền",
|
||||
domainKw: "Từ khóa Miền",
|
||||
domainRgx: "Regex Miền",
|
||||
ip: "CIDRs IP",
|
||||
privateIp: "Dải IP Không hợp lệ",
|
||||
port: "Cổng",
|
||||
portRange: "Dải Cổng",
|
||||
srcCidr: "CIDRs IP Nguồn",
|
||||
srcPrivateIp: "IP Nguồn Không hợp lệ",
|
||||
srcPort: "Cổng Nguồn",
|
||||
srcPortRange: "Dải Cổng Nguồn",
|
||||
ruleset: "Bộ quy tắc",
|
||||
rulesetMatchSrc: "Bộ quy tắc IPcidr Phù hợp Nguồn",
|
||||
preferredBy: "Ưu tiên theo đầu ra",
|
||||
interfaceAddr: "Địa chỉ giao diện",
|
||||
options: "Tùy chọn Quy tắc",
|
||||
domainRules: "Tên miền/IP",
|
||||
srcIpRules: "IP Nguồn",
|
||||
srcPortRules: "Cổng Nguồn",
|
||||
udpDisableDomainUnmapping: "Không màm mạng tiền lập tên miền",
|
||||
udpConnect: "Kết nối UDP",
|
||||
udpTimeout: "Thời gian Chờ UDP",
|
||||
method: "Phương pháp",
|
||||
noDrop: "Không Tháp",
|
||||
sniffer: "Kiểm tra Sniffer",
|
||||
timeout: "Thời gian Chờ Sniffing",
|
||||
strategy: "Chiến lệ",
|
||||
etaHint: "Mỗi dòng một mục. Dòng trống và mục trùng lặp sẽ bị bỏ qua.",
|
||||
import: {
|
||||
title: "Nhập hàng loạt bộ quy tắc",
|
||||
rulesTitle: "Nhập quy tắc",
|
||||
urlsHint: "Một URL mỗi dòng. Thẻ được lấy từ tên tệp (không gồm phần mở rộng).",
|
||||
fileHint: "Tải lên tệp .txt chứa URL, mỗi dòng một URL.",
|
||||
jsonHint: "Dán đối tượng JSON có mảng rules và/hoặc rule_set. Có thể dán cả khối \"route\": {'{'}...{'}'} hoặc chỉ nội dung bên trong.",
|
||||
fileJsonHint: "Tải lên tệp .json có khối route.",
|
||||
urlHint: "Nhập liên kết trực tiếp tới tệp JSON (ví dụ liên kết raw GitHub).",
|
||||
preview: "Xem trước",
|
||||
skipped: "đã tồn tại, hiển thị màu xám",
|
||||
conflict: "Phát hiện xung đột",
|
||||
merge: "Gộp — thêm quy tắc đã nhập (bỏ qua thẻ bộ trùng)",
|
||||
replace: "Thay thế — xóa quy tắc và bộ hiện có, nhập lại",
|
||||
pasteUrls: "Dán URL",
|
||||
uploadTxt: "Tải lên .txt",
|
||||
uploadFile: "Tải lên tệp",
|
||||
fromUrl: "Theo URL",
|
||||
selectTxt: "Chọn tệp .txt",
|
||||
selectJson: "Chọn tệp .json",
|
||||
parse: "Phân tích",
|
||||
conflictDesc: "Cấu hình đã có {rules} quy tắc và {rulesets} bộ quy tắc. Chọn thao tác:",
|
||||
finalOutbound: "Outbound mặc định (final)",
|
||||
applyFinal: "Áp dụng làm outbound mặc định",
|
||||
errNoArrays: 'Không tìm thấy mảng "rules" hoặc "rule_set".',
|
||||
errJsonParse: "Lỗi phân tích JSON: {message}",
|
||||
errNoArraysFetched: 'Không tìm thấy "rules" hoặc "rule_set" trong JSON đã tải.',
|
||||
errFetch: "Lỗi tải: {message}",
|
||||
errNoFile: "Chưa chọn tệp.",
|
||||
errNoArraysInFile: 'Không tìm thấy "rules" hoặc "rule_set" trong tệp.',
|
||||
},
|
||||
},
|
||||
ruleset: {
|
||||
add: "Thêm Bộ quy tắc",
|
||||
format: "Định dạng Dữ liệu",
|
||||
interval: "Khoảng cách Cập nhật",
|
||||
remote: "Xa",
|
||||
local: "Cục bộ",
|
||||
},
|
||||
dns: {
|
||||
add: "Thêm Máy chủ DNS",
|
||||
title: "Máy chủ DNS",
|
||||
final: "Cuối cùng",
|
||||
server: "Máy chủ",
|
||||
firstServer: "Máy chủ Đầu tiên",
|
||||
cacheCapacity: "Nội dung bộ nhớ",
|
||||
disableCache: "Vô hiệu hóa bộ nhớ đệm",
|
||||
disableExpire: "Vô hiệu hóa hệ thống",
|
||||
independentCache: "Bộ nhớ rẽ",
|
||||
reverseMapping: "Màm mạng tên lập",
|
||||
domainStrategy: "Chiến lược Domain",
|
||||
local: { preferGo: "Ưu tiên Go" },
|
||||
rule: {
|
||||
add: "Thêm Quy tắc DNS",
|
||||
title: "Quy tắc DNS",
|
||||
inet4Range: "Dải CIDR IPv4",
|
||||
inet6Range: "Dải CIDR IPv6",
|
||||
acceptDefault: "Chấp nhận Mặc định",
|
||||
action: {
|
||||
title: "Hành động",
|
||||
route: "Định tuyến",
|
||||
routeOptions: "Tùy chọn định tuyến",
|
||||
reject: "Từ chối",
|
||||
predefined: "Định nghĩa sẵn",
|
||||
rewriteTtl: "Ghi đè TTL",
|
||||
clientSubnet: "Subnet của máy khách",
|
||||
rcode: "Máy chủ tên",
|
||||
rcodes: {
|
||||
noError: "OK",
|
||||
formerr: "Yêu cầu không hợp lệ",
|
||||
servFail: "Lỗi máy chủ",
|
||||
nxDomain: "Không tìm thấy",
|
||||
refused: "Bị từ chối",
|
||||
notImp: "Chưa được hỗ trợ"
|
||||
},
|
||||
answer: "Câu trả lời",
|
||||
ns: "Máy chủ tên",
|
||||
extra: "Bổ sung"
|
||||
}
|
||||
}
|
||||
},
|
||||
basic: {
|
||||
log: {
|
||||
title: "Nhật ký",
|
||||
level: "Mức độ",
|
||||
output: "Đầu ra",
|
||||
timestamp: "Bật Dấu thời gian",
|
||||
},
|
||||
routing: {
|
||||
title: "Định tuyến",
|
||||
defaultOut: "Ra ngoài Mặc định",
|
||||
defaultIf: "NIC Mặc định",
|
||||
defaultRm: "Đánh dấu Định tuyến Mặc định",
|
||||
defaultDns: "Máy chủ DNS Mặc định",
|
||||
autoBind: "Tự động Ràng buộc NIC",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "Lưu IP Giả mạo",
|
||||
extController: "Trình điều khiển bên ngoài",
|
||||
extUi: "Giao diện người dùng bên ngoài",
|
||||
extUiDownloadUrl: "URL tải giao diện",
|
||||
extUiDownloadDetour: "Chuyển hướng tải giao diện",
|
||||
secret: "Mã bí mật",
|
||||
defaultMode: "Chế độ mặc định",
|
||||
allowOrigin: "Cho phép nguồn gốc",
|
||||
allowPrivate: "Cho phép mạng riêng",
|
||||
},
|
||||
},
|
||||
tls : {
|
||||
enable: "Kích hoạt TLS",
|
||||
usePath: "Sử dụng đường dẫn",
|
||||
useText: "Sử dụng văn bản",
|
||||
certPath: "Đường dẫn tệp chứng chỉ",
|
||||
keyPath: "Đường dẫn tệp khóa",
|
||||
cert: "Chứng chỉ",
|
||||
key: "Khóa",
|
||||
options: "Tùy chọn TLS",
|
||||
minVer: "Phiên bản Tối thiểu",
|
||||
maxVer: "Phiên bản Tối đa",
|
||||
cs: "Các bộ mã hóa",
|
||||
privKey: "Khóa riêng",
|
||||
pubKey: "Khóa Công khai",
|
||||
disableSni: "Tắt SNI",
|
||||
insecure: "Cho phép Không an toàn",
|
||||
fragment: "Kiểm tra hệ thống",
|
||||
fragmentDelay: "Khoảng thời gian hệ thống",
|
||||
recordFragment: "Kiểm tra bộ nhớ",
|
||||
store: "Kho lưu trữ gốc",
|
||||
ktls: "TLS nhân",
|
||||
kernelTx: "TX",
|
||||
kernelRx: "RX",
|
||||
queryServerName: "Tên máy chủ truy vấn ECH",
|
||||
acme: {
|
||||
options: "Tùy chọn ACME",
|
||||
dataDir: "Thư mục Dữ liệu",
|
||||
defaultDomain: "Tên miền Mặc định",
|
||||
disableChallenges: "Vô hiệu hóa Thách thức",
|
||||
httpChallenge: "Vô hiệu hóa Thách thức HTTP",
|
||||
tlsChallenge: "Vô hiệu hóa Thách thức TLS",
|
||||
altPorts: "Cổng Thay thế",
|
||||
altHport: "Cổng HTTP Thay thế",
|
||||
altTport: "Cổng TLS Thay thế",
|
||||
caProvider: "Nhà cung cấp CA",
|
||||
customCa: "Nhà cung cấp CA Tùy chỉnh",
|
||||
extAcc: "Tài khoản Bên ngoài",
|
||||
dns01: "Thách thức DNS01",
|
||||
dns01Provider: "Nhà cung cấp Thách thức DNS01",
|
||||
dns01Params: {
|
||||
api_token: "Mã API",
|
||||
zone_token: "Mã Vùng",
|
||||
access_key_id: "ID Khóa Truy cập",
|
||||
access_key_secret: "Bí mật Khóa Truy cập",
|
||||
region_id: "ID Khu vực",
|
||||
security_token: "Mã Bảo mật",
|
||||
username: "Tên đăng nhập",
|
||||
password: "Mật khẩu",
|
||||
subdomain: "Tên miền phụ",
|
||||
server_url: "URL Máy chủ",
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "Tải lên",
|
||||
download: "Tải xuống",
|
||||
volume: "Thể tích",
|
||||
usage: "Sử dụng",
|
||||
enable: "Kích hoạt thống kê",
|
||||
graphTitle: "Biểu đồ lưu lượng",
|
||||
B: "B",
|
||||
KB: "KB",
|
||||
MB: "MB",
|
||||
GB: "GB",
|
||||
TB: "TB",
|
||||
PB: "PB",
|
||||
p: "ph",
|
||||
Kp: "Kph",
|
||||
Mp: "Mph",
|
||||
Gp: "Gph",
|
||||
Mbps: "Mbps",
|
||||
},
|
||||
date: {
|
||||
expiry: "Hết hạn",
|
||||
expired: "Đã hết hạn",
|
||||
d: "ng",
|
||||
h: "g",
|
||||
m: "p",
|
||||
s: "s",
|
||||
ms: "ms",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
export default {
|
||||
success: "成功",
|
||||
failed: "失败",
|
||||
enable: "启用",
|
||||
disable: "禁用",
|
||||
none: "无",
|
||||
all: "全部",
|
||||
loading: "加载中...",
|
||||
confirm: "是否确定?",
|
||||
yes: "确认",
|
||||
no: "取消",
|
||||
unlimited: "无限",
|
||||
type: "类型",
|
||||
protocol: "协议",
|
||||
submit: "提交",
|
||||
reset: "重置",
|
||||
now: "当前",
|
||||
network: "网络",
|
||||
copyToClipboard: "复制到剪贴板",
|
||||
noData: "无数据!",
|
||||
invalidLogin: "登录无效!",
|
||||
online: "在线",
|
||||
version: "版本",
|
||||
email: "电子邮件",
|
||||
commaSeparated: "(逗号分隔)",
|
||||
count: "计数",
|
||||
template: "模板",
|
||||
editor: "编辑器",
|
||||
error: {
|
||||
dplData: "重复数据",
|
||||
core: "Sing-Box 错误",
|
||||
invalidData: "无效数据",
|
||||
},
|
||||
theme: {
|
||||
light: "浅色",
|
||||
dark: "深色",
|
||||
system: "跟随系统",
|
||||
},
|
||||
pages: {
|
||||
login: "登录",
|
||||
home: "主页",
|
||||
inbounds: "入站管理",
|
||||
outbounds: "出站管理",
|
||||
services: "服务管理",
|
||||
endpoints: "节点管理",
|
||||
clients: "用户管理",
|
||||
rules: "路由列表",
|
||||
tls: "TLS 设置",
|
||||
basics: "基础信息",
|
||||
dns: "DNS",
|
||||
admins: "管理员",
|
||||
settings: "设置",
|
||||
},
|
||||
main: {
|
||||
tiles: "信息卡",
|
||||
gauges: "仪表板",
|
||||
charts: "图表",
|
||||
infos: "信息",
|
||||
gauge: {
|
||||
cpu: "CPU 仪表",
|
||||
mem: "RAM 仪表",
|
||||
dsk: "Disk 仪表",
|
||||
swp: "Swap 仪表",
|
||||
},
|
||||
chart: {
|
||||
cpu: "CPU 监视器",
|
||||
mem: "RAM 监视器",
|
||||
net: "网络带宽",
|
||||
pnet: "网络数据包",
|
||||
dio: "Disk I/O",
|
||||
},
|
||||
info: {
|
||||
sys: "系统信息",
|
||||
sbd: "运行信息",
|
||||
host: "主机",
|
||||
cpu: "CPU",
|
||||
core: "核心",
|
||||
uptime: "运行时间",
|
||||
startupTime: "启动时间",
|
||||
threads: "线程",
|
||||
memory: "内存",
|
||||
running: "运行状态"
|
||||
},
|
||||
backup: {
|
||||
title: "备份与恢复",
|
||||
backup: "下载备份",
|
||||
restore: "恢复备份",
|
||||
exclStats: "排除图表数据",
|
||||
exclChanges: "排除变更数据",
|
||||
sbConfig: "下载 Sing-Box 配置",
|
||||
},
|
||||
stats: {
|
||||
title: "使用量与统计",
|
||||
totalUsage: "总用量",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "入站",
|
||||
client: "客户端",
|
||||
outbound: "出站",
|
||||
endpoint: "节点",
|
||||
config: "配置",
|
||||
rule: "规则",
|
||||
ruleset: "规则集",
|
||||
service: "服务",
|
||||
dnsserver: "DNS 服务器",
|
||||
dnsrule: "DNS规则",
|
||||
user: "用户",
|
||||
tag: "标签",
|
||||
listen: "监听",
|
||||
dial: "拨号",
|
||||
tls: "TLS",
|
||||
multiplex: "多路复用",
|
||||
transport: "传输",
|
||||
headers: "标头",
|
||||
key: "键",
|
||||
value: "值",
|
||||
},
|
||||
actions: {
|
||||
action: "操作",
|
||||
add: "添加",
|
||||
addbulk: "批量添加",
|
||||
editbulk: "批量编辑",
|
||||
delbulk: "批量删除",
|
||||
new: "新建",
|
||||
edit: "编辑",
|
||||
del: "删除",
|
||||
clone: "克隆",
|
||||
test: "测试",
|
||||
testAll: "测试全部",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
submit: "提交",
|
||||
set: "设置",
|
||||
generate: "生成",
|
||||
disable: "禁用",
|
||||
close: "关闭",
|
||||
restartApp: "重启面板",
|
||||
restartSb: "重启 Singbox",
|
||||
},
|
||||
login: {
|
||||
title: "登录",
|
||||
username: "用户名",
|
||||
unRules: "用户名不能为空",
|
||||
password: "密码",
|
||||
pwRules: "密码不能为空",
|
||||
},
|
||||
menu: {
|
||||
logout: "退出登录",
|
||||
},
|
||||
admin: {
|
||||
changeCred: "更改凭据",
|
||||
oldPass: "当前密码",
|
||||
newUname: "新用户名",
|
||||
newPass: "新密码",
|
||||
lastLogin: "上次登录",
|
||||
date: "日期",
|
||||
time: "时间",
|
||||
changes: "更改",
|
||||
actor: "执行者",
|
||||
key: "键",
|
||||
action: "操作",
|
||||
api: {
|
||||
title: "API 令牌",
|
||||
msg: "请复制令牌并保存到安全的地方。它将不再显示。",
|
||||
token: "令牌",
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
interface: "界面",
|
||||
sub: "订阅",
|
||||
addr: "地址",
|
||||
port: "端口",
|
||||
webPath: "面板路径",
|
||||
domain: "域名",
|
||||
sslKey: "SSL 密钥 (Key) 路径",
|
||||
sslCert: "SSL 证书 (cert) 路径",
|
||||
webUri: "面板 URI",
|
||||
sessionAge: "会话超时时限",
|
||||
trafficAge: "流量过期时限",
|
||||
timeLoc: "时区",
|
||||
subEncode: "启用 Base64 编码",
|
||||
subInfo: "启用用户信息",
|
||||
path: "默认路径",
|
||||
update: "自动更新时间",
|
||||
subUri: "订阅 URI",
|
||||
jsonSub: "JSON 订阅",
|
||||
toDirect: "路由到直连",
|
||||
toBlock: "路由到阻止",
|
||||
timestamp: "时间戳",
|
||||
globalDns: "全局 DNS",
|
||||
directDns: "直连 DNS",
|
||||
toDirectDns: "路由到直连 DNS",
|
||||
jsonSubOptions: "其他选项",
|
||||
excludePkg: "排除包",
|
||||
clashSub: "Clash 订阅",
|
||||
mixedPort: "混合入站端口",
|
||||
tun: "Tun 入站",
|
||||
},
|
||||
client: {
|
||||
name: "名称",
|
||||
desc: "描述",
|
||||
group: "组",
|
||||
inboundTags: "入站标签",
|
||||
basics: "基础",
|
||||
config: "配置",
|
||||
links: "链接",
|
||||
external: "外部链接",
|
||||
sub: "外部订阅",
|
||||
delayStart: "延迟启动",
|
||||
autoReset: "自动重置",
|
||||
resetDays: "重置天数",
|
||||
nextReset: "下次重置",
|
||||
},
|
||||
bulk: {
|
||||
order: "排序",
|
||||
random: "随机",
|
||||
changeLimits: "更改限制",
|
||||
addInbounds: "添加入站",
|
||||
removeInbounds: "移除入站",
|
||||
addDays: "增加天数",
|
||||
addVolume: "增加流量",
|
||||
},
|
||||
types: {
|
||||
un: "用户名",
|
||||
pw: "密码",
|
||||
direct: {
|
||||
overrideAddr: "覆盖地址",
|
||||
overridePort: "覆盖端口",
|
||||
},
|
||||
hy: {
|
||||
obfs: "混淆密码",
|
||||
auth: "认证密码",
|
||||
hyOptions: "Hysteria 选项",
|
||||
hy2Options: "Hysteria2 选项",
|
||||
ignoreBw: "忽略客户端带宽",
|
||||
},
|
||||
shdwTls: {
|
||||
hs: "握手服务器",
|
||||
addHS: "添加握手服务器",
|
||||
},
|
||||
ssh: {
|
||||
passphrase: "密码短语",
|
||||
hostKey: "主机密钥",
|
||||
algorithm: "密钥算法",
|
||||
clientVer: "客户端版本",
|
||||
options: "SSH 选项",
|
||||
},
|
||||
tor: {
|
||||
execPath: "可执行文件路径",
|
||||
dataDir: "数据目录",
|
||||
extArgs: "额外参数",
|
||||
},
|
||||
tuic: {
|
||||
congControl: "拥塞控制",
|
||||
authTimeout: "认证超时",
|
||||
hb: "心跳包",
|
||||
},
|
||||
tun: {
|
||||
addr: "地址",
|
||||
ifName: "接口名称",
|
||||
excludeMptcp: "排除 MPTCP",
|
||||
fallbackRuleIndex: "iproute2 回退规则索引",
|
||||
},
|
||||
vless: {
|
||||
flow: "流控",
|
||||
udpEnc: "UDP 数据包编码",
|
||||
},
|
||||
vmess: {
|
||||
security: "安全性",
|
||||
globalPadding: "全局填充",
|
||||
authLen: "加密长度",
|
||||
},
|
||||
wg: {
|
||||
privKey: "私钥",
|
||||
pubKey: "对等方公钥",
|
||||
psk: "预共享密钥",
|
||||
localIp: "本地 IP 地址",
|
||||
worker: "工作线程",
|
||||
ifName: "接口名称",
|
||||
sysIf: "系统接口",
|
||||
options: "WireGuard 选项",
|
||||
allowedIp: "允许的 IP 地址",
|
||||
peer: "对等体",
|
||||
peers: "对等体",
|
||||
},
|
||||
lb: {
|
||||
defaultOut: "默认出站",
|
||||
interruptConn: "中断现有连接",
|
||||
testUrl: "测试 URL",
|
||||
interval: "间隔",
|
||||
tolerance: "容错",
|
||||
urlTestOptions: "URLTest 选项",
|
||||
},
|
||||
ts: {
|
||||
options: "Tailscale 选项",
|
||||
stateDir: "状态目录",
|
||||
authKey: "认证密钥",
|
||||
relayServer: "中继服务器",
|
||||
relayServerPort: "中继服务器端口",
|
||||
relayEndpoints: "中继静态端点",
|
||||
systemInterface: "系统接口",
|
||||
sysIfName: "接口名称",
|
||||
sysIfMtu: "接口 MTU",
|
||||
controlUrl: "控制 URL",
|
||||
ephemeral: "临时节点",
|
||||
hostname: "主机名",
|
||||
acceptRoutes: "接受路由",
|
||||
exitNode: "出口节点",
|
||||
allowLanAccess: "允许 LAN 访问",
|
||||
advRoutes: "广告路由",
|
||||
advExitNode: "广告出口节点",
|
||||
udpTimeout: "UDP 超时",
|
||||
},
|
||||
ocm: {
|
||||
credentialPath: "凭据路径",
|
||||
usagesPath: "用量统计路径",
|
||||
users: "用户",
|
||||
userName: "名称",
|
||||
userToken: "令牌",
|
||||
},
|
||||
ccm: {
|
||||
credentialPath: "凭据路径",
|
||||
usagesPath: "用量统计路径",
|
||||
users: "用户",
|
||||
userName: "名称",
|
||||
userToken: "令牌",
|
||||
},
|
||||
derp: {
|
||||
configPath: "配置路径",
|
||||
verifyClientEndpoint: "验证客户端端点",
|
||||
verifyClientUrl: "验证客户端 URL",
|
||||
meshWith: "与其他 DERP 节点网格连接",
|
||||
meshPsk: "网格预共享密钥",
|
||||
meshPskFile: "网格预共享密钥文件",
|
||||
stun: "STUN 服务器",
|
||||
options: "DERP 选项",
|
||||
},
|
||||
naive: {
|
||||
insecureConcurrency: "不安全并发数",
|
||||
quic: "QUIC",
|
||||
quicCongestion: "QUIC 拥塞控制",
|
||||
udpOverTcp: "UDP over TCP",
|
||||
},
|
||||
anytls: {
|
||||
idleInterval: "空闲会话检查间隔",
|
||||
idleTimeout: "空闲会话超时",
|
||||
minIdle: "最小空闲会话数"
|
||||
},
|
||||
},
|
||||
in: {
|
||||
addr: "地址",
|
||||
port: "端口",
|
||||
ssMethod: "方法",
|
||||
ssManageable: "可管理的",
|
||||
sSide: "服务器端",
|
||||
cSide: "客户端",
|
||||
multiDomain: "多域名",
|
||||
remark: "备注",
|
||||
mdOption: "多域名选项",
|
||||
},
|
||||
listen: {
|
||||
options: "监听选项",
|
||||
tcpOptions: "TCP 选项",
|
||||
udpOptions: "UDP 选项",
|
||||
detour: "转发",
|
||||
detourText: "转发到入站",
|
||||
disableTcpKeepAlive: "禁用 TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "TCP Keep Alive 间隔",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "绑定到网络接口",
|
||||
bindIp4: "绑定到 IPv4",
|
||||
bindIp6: "绑定到 IPv6",
|
||||
bindNoPort: "绑定地址不占端口",
|
||||
reuseAddr: "重用监听地址",
|
||||
connTimeout: "连接超时",
|
||||
disableTcpKeepAlive: "禁用 TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "TCP Keep Alive 间隔",
|
||||
domainResolver: "域名解析器",
|
||||
options: "拨号选项",
|
||||
detourText: "转发至出站",
|
||||
},
|
||||
transport: {
|
||||
enable: "启用传输",
|
||||
host: "主机域名",
|
||||
hosts: "主机域名列表",
|
||||
path: "HTTP 请求路径",
|
||||
httpMethod: "HTTP 请求方法",
|
||||
idleTimeout: "空闲超时",
|
||||
pingTimeout: "Ping 超时",
|
||||
grpcServiceName: "gRPC 服务名称",
|
||||
grpcPws: "允许无流时保持连接",
|
||||
},
|
||||
mux: {
|
||||
enable: "启用多路复用",
|
||||
maxConn: "最大连接数",
|
||||
minStr: "最小流数",
|
||||
maxStr: "最大流数",
|
||||
padding: "仅允许填充连接",
|
||||
enableBrutal: "启用 TCP Brutal",
|
||||
},
|
||||
out: {
|
||||
addr: "服务器地址",
|
||||
port: "服务器端口",
|
||||
addUrlTest: "添加 URLTest",
|
||||
delay: "延迟",
|
||||
},
|
||||
rule: {
|
||||
add: "添加规则",
|
||||
simple: "简单",
|
||||
logical: "逻辑",
|
||||
mode: "模式",
|
||||
invert: "反选结果",
|
||||
ipVer: "IP 版本",
|
||||
domain: "域名",
|
||||
domainSufix: "域名后缀",
|
||||
domainKw: "域名关键词",
|
||||
domainRgx: "域名正则表达式",
|
||||
ip: "IP CIDR",
|
||||
privateIp: "匹配非公开 IP",
|
||||
port: "端口",
|
||||
portRange: "端口范围",
|
||||
srcCidr: "源 IP CIDR",
|
||||
srcPrivateIp: "匹配非公开源 IP",
|
||||
srcPort: "源端口",
|
||||
srcPortRange: "源端口范围",
|
||||
ruleset: "规则集",
|
||||
rulesetMatchSrc: "规则集 IP CIDR 匹配源 IP",
|
||||
preferredBy: "优选出站",
|
||||
interfaceAddr: "接口地址",
|
||||
options: "规则选项",
|
||||
domainRules: "域名/IP",
|
||||
srcIpRules: "源 IP",
|
||||
srcPortRules: "源端口",
|
||||
udpDisableDomainUnmapping: "禁用域名解析映射",
|
||||
udpConnect: "启用 UDP 连接",
|
||||
udpTimeout: "UDP 超时",
|
||||
method: "方法",
|
||||
noDrop: "不丢弃",
|
||||
sniffer: "嗅探",
|
||||
timeout: "超时",
|
||||
strategy: "策略",
|
||||
etaHint: "每行一项。空行和重复项将被忽略。",
|
||||
import: {
|
||||
title: "批量导入规则集",
|
||||
rulesTitle: "导入规则",
|
||||
urlsHint: "每行一个 URL。标签由文件名(不含扩展名)决定。",
|
||||
fileHint: "上传包含 URL 的 .txt 文件,每行一个。",
|
||||
jsonHint: "粘贴包含 rules 和/或 rule_set 数组的 JSON 对象。可粘贴整个 \"route\" 块:{'{'}...{'}'} 或仅其内容。",
|
||||
fileJsonHint: "上传包含 route 块的 .json 文件。",
|
||||
urlHint: "填写 JSON 文件的直链(例如 GitHub raw 链接)。",
|
||||
preview: "预览",
|
||||
skipped: "已存在,以灰色显示",
|
||||
conflict: "检测到冲突",
|
||||
merge: "合并 — 添加导入的规则(跳过重复的规则集标签)",
|
||||
replace: "替换 — 删除现有规则与规则集后重新导入",
|
||||
pasteUrls: "粘贴 URL",
|
||||
uploadTxt: "上传 .txt",
|
||||
uploadFile: "上传文件",
|
||||
fromUrl: "通过链接",
|
||||
selectTxt: "选择 .txt 文件",
|
||||
selectJson: "选择 .json 文件",
|
||||
parse: "解析",
|
||||
conflictDesc: "配置中已有 {rules} 条规则和 {rulesets} 个规则集。请选择操作:",
|
||||
finalOutbound: "默认出站(final)",
|
||||
applyFinal: "设为默认出站",
|
||||
errNoArrays: '未找到 "rules" 或 "rule_set" 数组。',
|
||||
errJsonParse: "JSON 解析错误:{message}",
|
||||
errNoArraysFetched: '获取的 JSON 中未找到 "rules" 或 "rule_set"。',
|
||||
errFetch: "获取失败:{message}",
|
||||
errNoFile: "未选择文件。",
|
||||
errNoArraysInFile: '文件中未找到 "rules" 或 "rule_set"。',
|
||||
},
|
||||
},
|
||||
ruleset: {
|
||||
add: "添加规则集",
|
||||
format: "数据格式",
|
||||
interval: "更新间隔",
|
||||
remote: "远程",
|
||||
local: "本地",
|
||||
},
|
||||
dns: {
|
||||
add: "添加 DNS 服务器",
|
||||
title: "DNS 服务器",
|
||||
final: "最终",
|
||||
server: "服务器",
|
||||
firstServer: "首选服务器",
|
||||
cacheCapacity: "缓存容量",
|
||||
disableCache: "禁用缓存",
|
||||
disableExpire: "禁用过期",
|
||||
independentCache: "独立缓存",
|
||||
reverseMapping: "反向映射",
|
||||
domainStrategy: "域名解析策略",
|
||||
local: { preferGo: "优先使用 Go" },
|
||||
rule: {
|
||||
add: "添加 DNS 规则",
|
||||
title: "DNS 规则",
|
||||
inet4Range: "IPv4 范围",
|
||||
inet6Range: "IPv6 范围",
|
||||
acceptDefault: "接受默认",
|
||||
action: {
|
||||
title: "操作",
|
||||
route: "路由",
|
||||
routeOptions: "路由选项",
|
||||
reject: "拒绝",
|
||||
predefined: "预定义",
|
||||
rewriteTtl: "重写 TTL",
|
||||
clientSubnet: "客户端子网",
|
||||
rcode: "响应码",
|
||||
rcodes: {
|
||||
noError: "正常",
|
||||
formerr: "请求错误",
|
||||
servFail: "服务器故障",
|
||||
nxDomain: "未找到",
|
||||
refused: "被拒绝",
|
||||
notImp: "未实现"
|
||||
},
|
||||
answer: "回答",
|
||||
ns: "名称服务器",
|
||||
extra: "附加"
|
||||
}
|
||||
}
|
||||
},
|
||||
basic: {
|
||||
log: {
|
||||
title: "日志",
|
||||
level: "级别",
|
||||
output: "输出",
|
||||
timestamp: "启用时间戳",
|
||||
},
|
||||
routing: {
|
||||
title: "路由",
|
||||
defaultOut: "默认出站",
|
||||
defaultIf: "默认网卡",
|
||||
defaultRm: "默认路由标记",
|
||||
defaultDns: "默认 DNS 解析器",
|
||||
autoBind: "自动绑定网卡",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "持久化 Fake-IP",
|
||||
extController: "外部控制器",
|
||||
extUi: "外部界面",
|
||||
extUiDownloadUrl: "界面下载地址",
|
||||
extUiDownloadDetour: "界面下载绕行",
|
||||
secret: "密钥",
|
||||
defaultMode: "默认模式",
|
||||
allowOrigin: "允许来源",
|
||||
allowPrivate: "允许私有网络",
|
||||
},
|
||||
},
|
||||
tls : {
|
||||
enable: "启用 TLS",
|
||||
usePath: "使用外部路径",
|
||||
useText: "使用文件内容",
|
||||
certPath: "证书文件路径",
|
||||
keyPath: "私钥文件路径",
|
||||
cert: "证书文件内容",
|
||||
key: "私钥文件内容",
|
||||
options: "TLS 选项",
|
||||
minVer: "最低版本",
|
||||
maxVer: "最高版本",
|
||||
cs: "密码套件",
|
||||
privKey: "私钥",
|
||||
pubKey: "公钥",
|
||||
disableSni: "禁用 SNI",
|
||||
insecure: "允许不安全",
|
||||
fragment: "启用",
|
||||
fragmentDelay: "启用后延迟",
|
||||
recordFragment: "启用",
|
||||
store: "根证书库",
|
||||
ktls: "内核 TLS",
|
||||
kernelTx: "发送",
|
||||
kernelRx: "接收",
|
||||
queryServerName: "ECH 查询服务器名称",
|
||||
acme: {
|
||||
options: "ACME 选项",
|
||||
dataDir: "数据目录",
|
||||
defaultDomain: "默认域名",
|
||||
disableChallenges: "禁用挑战",
|
||||
httpChallenge: "禁用 HTTP 挑战",
|
||||
tlsChallenge: "禁用 TLS 挑战",
|
||||
altPorts: "替代端口",
|
||||
altHport: "替代 HTTP 端口",
|
||||
altTport: "替代 TLS 端口",
|
||||
caProvider: "CA 提供商",
|
||||
customCa: "自定义 CA 提供商",
|
||||
extAcc: "外部账户",
|
||||
dns01: "DNS01 挑战",
|
||||
dns01Provider: "DNS01 挑战提供商",
|
||||
dns01Params: {
|
||||
api_token: "API 令牌",
|
||||
zone_token: "Zone 令牌",
|
||||
access_key_id: "访问密钥 ID",
|
||||
access_key_secret: "访问密钥密文",
|
||||
region_id: "区域 ID",
|
||||
security_token: "安全令牌",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
subdomain: "子域名",
|
||||
server_url: "服务器 URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "上传",
|
||||
download: "下载",
|
||||
volume: "流量",
|
||||
usage: "已用",
|
||||
enable: "启用统计",
|
||||
graphTitle: "流量图表",
|
||||
B: "B",
|
||||
KB: "KB",
|
||||
MB: "MB",
|
||||
GB: "GB",
|
||||
TB: "TB",
|
||||
PB: "PB",
|
||||
p: "p",
|
||||
Kp: "Kp",
|
||||
Mp: "Mp",
|
||||
Gp: "Gp",
|
||||
Mbps: "Mbps",
|
||||
},
|
||||
date: {
|
||||
expiry: "到期",
|
||||
expired: "已到期",
|
||||
d: "天",
|
||||
h: "时",
|
||||
m: "分",
|
||||
s: "秒",
|
||||
ms: "毫秒",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
export default {
|
||||
success: "成功",
|
||||
failed: "失敗",
|
||||
enable: "啟用",
|
||||
disable: "禁用",
|
||||
none: "無",
|
||||
all: "全部",
|
||||
loading: "加載中...",
|
||||
confirm: "是否確定?",
|
||||
yes: "確認",
|
||||
no: "取消",
|
||||
unlimited: "無限",
|
||||
type: "類型",
|
||||
protocol: "協定",
|
||||
submit: "提交",
|
||||
reset: "重置",
|
||||
now: "當前",
|
||||
network: "網絡",
|
||||
copyToClipboard: "復製到剪貼板",
|
||||
noData: "無數據!",
|
||||
invalidLogin: "登錄無效!",
|
||||
online: "在線",
|
||||
version: "版本",
|
||||
email: "電子郵件",
|
||||
commaSeparated: "(逗號分隔)",
|
||||
count: "計數",
|
||||
template: "模板",
|
||||
editor: "編輯器",
|
||||
error: {
|
||||
dplData: "重複數據",
|
||||
core: "Sing-Box 錯誤",
|
||||
invalidData: "無效數據",
|
||||
},
|
||||
theme: {
|
||||
light: "明亮",
|
||||
dark: "暗黑",
|
||||
system: "系統",
|
||||
},
|
||||
pages: {
|
||||
login: "登錄",
|
||||
home: "主頁",
|
||||
inbounds: "入站管理",
|
||||
outbounds: "出站管理",
|
||||
services: "服務管理",
|
||||
endpoints: "端點管理",
|
||||
clients: "用戶管理",
|
||||
rules: "路由列表",
|
||||
tls: "TLS 設置",
|
||||
basics: "基礎信息",
|
||||
dns: "DNS",
|
||||
admins: "管理員",
|
||||
settings: "設置",
|
||||
},
|
||||
main: {
|
||||
tiles: "信息卡",
|
||||
gauges: "儀表板",
|
||||
charts: "圖表",
|
||||
infos: "信息",
|
||||
gauge: {
|
||||
cpu: "CPU 儀表",
|
||||
mem: "RAM 儀表",
|
||||
dsk: "Disk 儀表",
|
||||
swp: "Swap 儀表",
|
||||
},
|
||||
chart: {
|
||||
cpu: "CPU 監視器",
|
||||
mem: "RAM 監視器",
|
||||
net: "網絡帶寬",
|
||||
pnet: "網絡數據包",
|
||||
dio: "Disk I/O",
|
||||
},
|
||||
info: {
|
||||
sys: "系統信息",
|
||||
sbd: "運行信息",
|
||||
host: "主機",
|
||||
cpu: "CPU",
|
||||
core: "核心",
|
||||
uptime: "運行時間",
|
||||
startupTime: "啟動時間",
|
||||
threads: "線程",
|
||||
memory: "內存",
|
||||
running: "運行狀態"
|
||||
},
|
||||
backup: {
|
||||
title: "備份與恢復",
|
||||
backup: "下載備份",
|
||||
restore: "恢復備份",
|
||||
exclStats: "排除圖表記錄",
|
||||
exclChanges: "排除更改記錄",
|
||||
sbConfig: "下載 Sing-Box 配置",
|
||||
},
|
||||
stats: {
|
||||
title: "使用量與統計",
|
||||
totalUsage: "總用量",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "入站",
|
||||
client: "客戶端",
|
||||
outbound: "出站",
|
||||
endpoint: "端點",
|
||||
config: "配置",
|
||||
rule: "規則",
|
||||
ruleset: "規則集",
|
||||
service: "服務",
|
||||
dnsserver: "DNS 服務器",
|
||||
dnsrule: "DNS 規則",
|
||||
user: "用戶",
|
||||
tag: "標簽",
|
||||
listen: "聽",
|
||||
dial: "撥號",
|
||||
tls: "TLS",
|
||||
multiplex: "多路復用",
|
||||
transport: "傳輸",
|
||||
headers: "方法",
|
||||
key: "鑰匙",
|
||||
value: "價值",
|
||||
},
|
||||
actions: {
|
||||
action: "操作",
|
||||
add: "添加",
|
||||
addbulk: "批量添加",
|
||||
editbulk: "批量編輯",
|
||||
delbulk: "批量刪除",
|
||||
new: "新建",
|
||||
edit: "編輯",
|
||||
del: "刪除",
|
||||
clone: "克隆",
|
||||
test: "測試",
|
||||
testAll: "測試全部",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
submit: "提交",
|
||||
set: "設置",
|
||||
generate: "生成",
|
||||
disable: "禁用",
|
||||
close: "關閉",
|
||||
restartApp: "重啟面板",
|
||||
restartSb: "重啟 Singbox",
|
||||
},
|
||||
login: {
|
||||
title: "登錄",
|
||||
username: "用戶名",
|
||||
unRules: "用戶名不能為空",
|
||||
password: "密碼",
|
||||
pwRules: "密碼不能為空",
|
||||
},
|
||||
menu: {
|
||||
logout: "退出登錄",
|
||||
},
|
||||
admin: {
|
||||
changeCred: "更改憑據",
|
||||
oldPass: "當前密碼",
|
||||
newUname: "新用戶名",
|
||||
newPass: "新密碼",
|
||||
lastLogin: "上次登入",
|
||||
date: "日期",
|
||||
time: "時間",
|
||||
changes: "更改",
|
||||
actor: "執行者",
|
||||
key: "鍵",
|
||||
action: "操作",
|
||||
api: {
|
||||
title: "API 憑據",
|
||||
msg: "請複製下面的憑據並儲存在安全的地方。它將不再顯示。",
|
||||
token: "憑據",
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
interface: "界面",
|
||||
sub: "訂閱",
|
||||
addr: "地址",
|
||||
port: "端口",
|
||||
webPath: "基本 URI",
|
||||
domain: "域名",
|
||||
sslKey: "SSL 密鑰 (Key) 路徑",
|
||||
sslCert: "SSL 證書 (cert) 路徑",
|
||||
webUri: "面板 URI",
|
||||
sessionAge: "會話最大連接數",
|
||||
trafficAge: "流量最大年齡",
|
||||
timeLoc: "時區",
|
||||
subEncode: "啟用編碼",
|
||||
subInfo: "啟用用戶信息",
|
||||
path: "默認路徑",
|
||||
update: "自動更新時間",
|
||||
subUri: "訂閱 URL",
|
||||
jsonSub: "JSON 訂閱",
|
||||
toDirect: "路由到直連",
|
||||
toBlock: "路由到阻止",
|
||||
timestamp: "時間戳",
|
||||
globalDns: "全局 DNS",
|
||||
directDns: "直連 DNS",
|
||||
toDirectDns: "路由到直連 DNS",
|
||||
jsonSubOptions: "其他選項",
|
||||
excludePkg: "排除包",
|
||||
clashSub: "Clash 訂閱",
|
||||
mixedPort: "混合入站端口",
|
||||
tun: "Tun 入站",
|
||||
},
|
||||
client: {
|
||||
name: "名稱",
|
||||
desc: "描述",
|
||||
group: "組",
|
||||
inboundTags: "入站標簽",
|
||||
basics: "基礎",
|
||||
config: "配置",
|
||||
links: "鏈接",
|
||||
external: "外部鏈接",
|
||||
sub: "外部訂閱",
|
||||
delayStart: "延遲啟動",
|
||||
autoReset: "自動重置",
|
||||
resetDays: "重置天數",
|
||||
nextReset: "下次重置",
|
||||
},
|
||||
bulk: {
|
||||
order: "排序",
|
||||
random: "隨機",
|
||||
changeLimits: "變更限制",
|
||||
addInbounds: "添加入站",
|
||||
removeInbounds: "移除入站",
|
||||
addDays: "增加天數",
|
||||
addVolume: "增加流量",
|
||||
},
|
||||
types: {
|
||||
un: "用戶名",
|
||||
pw: "密碼",
|
||||
direct: {
|
||||
overrideAddr: "覆蓋地址",
|
||||
overridePort: "覆蓋端口",
|
||||
},
|
||||
hy: {
|
||||
obfs: "混淆密碼",
|
||||
auth: "驗證密碼",
|
||||
hyOptions: "Hysteria 選項",
|
||||
hy2Options: "Hysteria2 選項",
|
||||
ignoreBw: "忽略客戶端帶寬",
|
||||
},
|
||||
shdwTls: {
|
||||
hs: "握手服務器",
|
||||
addHS: "添加握手服務器",
|
||||
},
|
||||
ssh: {
|
||||
passphrase: "密語",
|
||||
hostKey: "主機密鑰",
|
||||
algorithm: "密鑰算法",
|
||||
clientVer: "客戶端版本",
|
||||
options: "SSH 選項",
|
||||
},
|
||||
tor: {
|
||||
execPath: "可執行文件路徑",
|
||||
dataDir: "數據目錄",
|
||||
extArgs: "額外參數",
|
||||
},
|
||||
tuic: {
|
||||
congControl: "擁塞控制",
|
||||
authTimeout: "身份驗證超時",
|
||||
hb: "心跳",
|
||||
},
|
||||
tun: {
|
||||
addr: "地址",
|
||||
ifName: "介面名稱",
|
||||
excludeMptcp: "排除 MPTCP",
|
||||
fallbackRuleIndex: "iproute2 回退規則索引",
|
||||
},
|
||||
vless: {
|
||||
flow: "流量",
|
||||
udpEnc: "UDP 封包編碼",
|
||||
},
|
||||
vmess: {
|
||||
security: "安全性",
|
||||
globalPadding: "全局填充",
|
||||
authLen: "加密長度",
|
||||
},
|
||||
wg: {
|
||||
privKey: "私鑰",
|
||||
pubKey: "對等方公鑰",
|
||||
psk: "預共享密鑰",
|
||||
localIp: "本地 IP",
|
||||
worker: "工作線程",
|
||||
ifName: "介面名稱",
|
||||
sysIf: "系統介面",
|
||||
options: "Wireguard 選項",
|
||||
allowedIp: "允許的 IP",
|
||||
peer: "對等方",
|
||||
peers: "對等方",
|
||||
},
|
||||
lb: {
|
||||
defaultOut: "默認外部",
|
||||
interruptConn: "中斷現有連接",
|
||||
testUrl: "測試 URL",
|
||||
interval: "間隔",
|
||||
tolerance: "容忍度",
|
||||
urlTestOptions: "URLTest 選項"
|
||||
},
|
||||
ts: {
|
||||
options: "Tailscale 選項",
|
||||
stateDir: "狀態目錄",
|
||||
authKey: "授權密鑰",
|
||||
relayServer: "轉發伺服器",
|
||||
relayServerPort: "轉發伺服器端口",
|
||||
relayEndpoints: "轉發靜態端點",
|
||||
systemInterface: "系統介面",
|
||||
sysIfName: "介面名稱",
|
||||
sysIfMtu: "介面 MTU",
|
||||
controlUrl: "控制 URL",
|
||||
ephemeral: "臨時節點",
|
||||
hostname: "主機名",
|
||||
acceptRoutes: "接受路由",
|
||||
exitNode: "出口節點",
|
||||
allowLanAccess: "允許 LAN 訪問",
|
||||
advRoutes: "廣告路由",
|
||||
advExitNode: "廣告出口節點",
|
||||
udpTimeout: "UDP 超時",
|
||||
},
|
||||
ocm: {
|
||||
credentialPath: "憑證路徑",
|
||||
usagesPath: "用量統計路徑",
|
||||
users: "用戶",
|
||||
userName: "名稱",
|
||||
userToken: "令牌",
|
||||
},
|
||||
ccm: {
|
||||
credentialPath: "憑證路徑",
|
||||
usagesPath: "用量統計路徑",
|
||||
users: "用戶",
|
||||
userName: "名稱",
|
||||
userToken: "令牌",
|
||||
},
|
||||
derp: {
|
||||
configPath: "配置路徑",
|
||||
verifyClientEndpoint: "驗證客戶端端點",
|
||||
verifyClientUrl: "驗證客戶端 URL",
|
||||
meshWith: "網狀連接",
|
||||
meshPsk: "網狀 PSK",
|
||||
meshPskFile: "網狀 PSK 文件",
|
||||
stun: "STUN 服務器",
|
||||
options: "DERP 選項",
|
||||
},
|
||||
naive: {
|
||||
insecureConcurrency: "不安全並發數",
|
||||
quic: "QUIC",
|
||||
quicCongestion: "QUIC 擁塞控制",
|
||||
udpOverTcp: "UDP over TCP",
|
||||
},
|
||||
anytls: {
|
||||
idleInterval: "閒置會話檢查間隔",
|
||||
idleTimeout: "閒置會話逾時",
|
||||
minIdle: "最小閒置會話數"
|
||||
},
|
||||
},
|
||||
in: {
|
||||
addr: "地址",
|
||||
port: "端口",
|
||||
ssMethod: "方法",
|
||||
ssManageable: "可管理的",
|
||||
sSide: "服務器端",
|
||||
cSide: "客戶端",
|
||||
multiDomain: "多域名",
|
||||
remark: "備註",
|
||||
mdOption: "多域名選項",
|
||||
},
|
||||
listen: {
|
||||
options: "監聽選項",
|
||||
tcpOptions: "TCP 選項",
|
||||
udpOptions: "UDP 選項",
|
||||
detour: "繞道",
|
||||
detourText: "轉發到入站",
|
||||
disableTcpKeepAlive: "停用 TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "TCP Keep Alive 間隔",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "綁定到網路接口",
|
||||
bindIp4: "綁定到 IPv4",
|
||||
bindIp6: "綁定到 IPv6",
|
||||
bindNoPort: "綁定地址不佔用端口",
|
||||
reuseAddr: "重用監聽地址",
|
||||
connTimeout: "連接超時",
|
||||
disableTcpKeepAlive: "停用 TCP Keep Alive",
|
||||
tcpKeepAlive: "TCP Keep Alive",
|
||||
tcpKeepAliveInterval: "TCP Keep Alive 間隔",
|
||||
domainResolver: "域名解析器",
|
||||
options: "撥號選項",
|
||||
detourText: "轉寄至出站",
|
||||
},
|
||||
transport: {
|
||||
enable: "啟用傳輸",
|
||||
host: "主機",
|
||||
hosts: "主機列表",
|
||||
path: "路徑",
|
||||
httpMethod: "請求方法",
|
||||
idleTimeout: "閒置超時",
|
||||
pingTimeout: "Ping 超時",
|
||||
grpcServiceName: "服務名稱",
|
||||
grpcPws: "允許無流",
|
||||
},
|
||||
mux: {
|
||||
enable: "啟用多路徑",
|
||||
maxConn: "最大連接數",
|
||||
minStr: "最小串流數",
|
||||
maxStr: "最大串流數",
|
||||
padding: "僅填充",
|
||||
enableBrutal: "啟用暴力",
|
||||
},
|
||||
out: {
|
||||
addr: "伺服器地址",
|
||||
port: "伺服器端口",
|
||||
addUrlTest: "新增 URLTest",
|
||||
delay: "延遲",
|
||||
},
|
||||
rule: {
|
||||
add: "添加規則",
|
||||
simple: "簡單",
|
||||
logical: "邏輯",
|
||||
mode: "模式",
|
||||
invert: "反轉",
|
||||
ipVer: "IP 版本",
|
||||
domain: "域名",
|
||||
domainSufix: "域名後綴",
|
||||
domainKw: "域名關鍵詞",
|
||||
domainRgx: "域名正則表達式",
|
||||
ip: "IP CIDR",
|
||||
privateIp: "無效 IP 範圍",
|
||||
port: "端口",
|
||||
portRange: "端口範圍",
|
||||
srcCidr: "源 IP CIDR",
|
||||
srcPrivateIp: "無效源 IP",
|
||||
srcPort: "源端口",
|
||||
srcPortRange: "源端口範圍",
|
||||
ruleset: "規則集",
|
||||
rulesetMatchSrc: "規則集 IP 範圍匹配源",
|
||||
preferredBy: "優選出站",
|
||||
interfaceAddr: "介面地址",
|
||||
options: "規則選項",
|
||||
domainRules: "域名/IP",
|
||||
srcIpRules: "源 IP",
|
||||
srcPortRules: "源端口",
|
||||
udpDisableDomainUnmapping: "禁用域名解析映射",
|
||||
udpConnect: "啟用 UDP 連接",
|
||||
udpTimeout: "UDP 超時",
|
||||
method: "方法",
|
||||
noDrop: "不丟弃",
|
||||
sniffer: "嗅探",
|
||||
timeout: "超時",
|
||||
strategy: "策略",
|
||||
etaHint: "每行一項。空白行與重複項目將被略過。",
|
||||
import: {
|
||||
title: "批次匯入規則集",
|
||||
rulesTitle: "匯入規則",
|
||||
urlsHint: "每行一個 URL。標籤由檔名(不含副檔名)決定。",
|
||||
fileHint: "上傳含 URL 的 .txt 檔案,每行一個。",
|
||||
jsonHint: "貼上含 rules 及/或 rule_set 陣列的 JSON 物件。可貼上整個 \"route\" 區塊:{'{'}...{'}'} 或僅其內容。",
|
||||
fileJsonHint: "上傳含 route 區塊的 .json 檔案。",
|
||||
urlHint: "填寫 JSON 檔案的直連(例如 GitHub raw 連結)。",
|
||||
preview: "預覽",
|
||||
skipped: "已存在,以灰色顯示",
|
||||
conflict: "偵測到衝突",
|
||||
merge: "合併 — 加入匯入的規則(略過重複的規則集標籤)",
|
||||
replace: "取代 — 刪除現有規則與規則集後重新匯入",
|
||||
pasteUrls: "貼上 URL",
|
||||
uploadTxt: "上傳 .txt",
|
||||
uploadFile: "上傳檔案",
|
||||
fromUrl: "透過連結",
|
||||
selectTxt: "選擇 .txt 檔案",
|
||||
selectJson: "選擇 .json 檔案",
|
||||
parse: "解析",
|
||||
conflictDesc: "設定中已有 {rules} 條規則與 {rulesets} 個規則集。請選擇操作:",
|
||||
finalOutbound: "預設出站(final)",
|
||||
applyFinal: "設為預設出站",
|
||||
errNoArrays: '找不到 "rules" 或 "rule_set" 陣列。',
|
||||
errJsonParse: "JSON 解析錯誤:{message}",
|
||||
errNoArraysFetched: '取得的 JSON 中找不到 "rules" 或 "rule_set"。',
|
||||
errFetch: "取得失敗:{message}",
|
||||
errNoFile: "未選擇檔案。",
|
||||
errNoArraysInFile: '檔案中找不到 "rules" 或 "rule_set"。',
|
||||
},
|
||||
},
|
||||
ruleset: {
|
||||
add: "添加規則集",
|
||||
format: "數據格式",
|
||||
interval: "更新間隔",
|
||||
remote: "遠端",
|
||||
local: "本地",
|
||||
},
|
||||
dns: {
|
||||
add: "添加 DNS 服務器",
|
||||
title: "DNS 服務器",
|
||||
final: "最終",
|
||||
server: "服務器",
|
||||
firstServer: "首選服務器",
|
||||
cacheCapacity: "快取容量",
|
||||
disableCache: "停用快取",
|
||||
disableExpire: "停用過期",
|
||||
independentCache: "獨立快取",
|
||||
reverseMapping: "反向映射",
|
||||
domainStrategy: "域名策略",
|
||||
local: { preferGo: "優先使用 Go" },
|
||||
rule: {
|
||||
add: "添加 DNS 規則",
|
||||
title: "DNS 規則",
|
||||
inet4Range: "IPv4 範圍",
|
||||
inet6Range: "IPv6 範圍",
|
||||
acceptDefault: "接受默認",
|
||||
action: {
|
||||
title: "操作",
|
||||
route: "路由",
|
||||
routeOptions: "路由選項",
|
||||
reject: "拒絕",
|
||||
predefined: "預設",
|
||||
rewriteTtl: "重寫 TTL",
|
||||
clientSubnet: "用戶端子網",
|
||||
rcode: "回應碼",
|
||||
rcodes: {
|
||||
noError: "正常",
|
||||
formerr: "請求錯誤",
|
||||
servFail: "伺服器故障",
|
||||
nxDomain: "未找到",
|
||||
refused: "被拒絕",
|
||||
notImp: "尚未實作"
|
||||
},
|
||||
answer: "回應",
|
||||
ns: "名稱伺服器",
|
||||
extra: "額外資訊"
|
||||
}
|
||||
},
|
||||
},
|
||||
basic: {
|
||||
log: {
|
||||
title: "日誌",
|
||||
level: "級別",
|
||||
output: "輸出",
|
||||
timestamp: "啟用時間戳記",
|
||||
},
|
||||
routing: {
|
||||
title: "路由",
|
||||
defaultOut: "默認外部",
|
||||
defaultIf: "默認網卡",
|
||||
defaultRm: "默認路由標記",
|
||||
defaultDns: "默認 DNS 解析器",
|
||||
autoBind: "自動綁定網卡",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "存儲假 IP",
|
||||
extController: "外部控制器",
|
||||
extUi: "外部介面",
|
||||
extUiDownloadUrl: "介面下載網址",
|
||||
extUiDownloadDetour: "介面下載繞行",
|
||||
secret: "密鑰",
|
||||
defaultMode: "預設模式",
|
||||
allowOrigin: "允許來源",
|
||||
allowPrivate: "允許私人網路",
|
||||
},
|
||||
},
|
||||
tls : {
|
||||
enable: "啟用 TLS",
|
||||
usePath: "使用外部路徑",
|
||||
useText: "使用文件內容",
|
||||
certPath: "證書文件路徑",
|
||||
keyPath: "私鑰文件路徑",
|
||||
cert: "證書文件內容",
|
||||
key: "私鑰文件內容",
|
||||
options: "TLS 選項",
|
||||
minVer: "最低版本",
|
||||
maxVer: "最高版本",
|
||||
cs: "加密套件",
|
||||
privKey: "私鑰",
|
||||
pubKey: "公鑰",
|
||||
disableSni: "停用 SNI",
|
||||
insecure: "允許不安全連線",
|
||||
fragment: "分段",
|
||||
fragmentDelay: "分段回應延遲",
|
||||
recordFragment: "多筆記錄分段",
|
||||
store: "根憑證庫",
|
||||
ktls: "內核 TLS",
|
||||
kernelTx: "發送",
|
||||
kernelRx: "接收",
|
||||
queryServerName: "ECH 查詢伺服器名稱",
|
||||
acme: {
|
||||
options: "ACME 選項",
|
||||
dataDir: "數據目錄",
|
||||
defaultDomain: "默認域名",
|
||||
disableChallenges: "禁用挑戰",
|
||||
httpChallenge: "禁用 HTTP 挑戰",
|
||||
tlsChallenge: "禁用 TLS 挑戰",
|
||||
altPorts: "替代端口",
|
||||
altHport: "替代 HTTP 端口",
|
||||
altTport: "替代 TLS 端口",
|
||||
caProvider: "CA 提供商",
|
||||
customCa: "自定義 CA 提供商",
|
||||
extAcc: "外部賬戶",
|
||||
dns01: "DNS01 挑戰",
|
||||
dns01Provider: "DNS01 挑戰提供商",
|
||||
dns01Params: {
|
||||
api_token: "API 令牌",
|
||||
zone_token: "Zone 令牌",
|
||||
access_key_id: "存取金鑰 ID",
|
||||
access_key_secret: "存取金鑰密文",
|
||||
region_id: "區域 ID",
|
||||
security_token: "安全令牌",
|
||||
username: "用戶名",
|
||||
password: "密碼",
|
||||
subdomain: "子網域",
|
||||
server_url: "伺服器 URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "上傳",
|
||||
download: "下載",
|
||||
volume: "流量",
|
||||
usage: "已用",
|
||||
enable: "啟用統計",
|
||||
graphTitle: "流量圖表",
|
||||
B: "B",
|
||||
KB: "KB",
|
||||
MB: "MB",
|
||||
GB: "GB",
|
||||
TB: "TB",
|
||||
PB: "PB",
|
||||
p: "p",
|
||||
Kp: "Kp",
|
||||
Mp: "Mp",
|
||||
Gp: "Gp",
|
||||
Mbps: "Mbps",
|
||||
},
|
||||
date: {
|
||||
expiry: "到期",
|
||||
expired: "已到期",
|
||||
d: "天",
|
||||
h: "時",
|
||||
m: "分",
|
||||
s: "秒",
|
||||
ms: "毫秒",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import { createApp, ref } from 'vue'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Use router
|
||||
import router from './router'
|
||||
|
||||
// Store
|
||||
import store from './store'
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Locale
|
||||
import { i18n } from '@/locales'
|
||||
import Vue3PersianDatetimePicker from 'vue3-persian-datetime-picker'
|
||||
|
||||
// Notivue
|
||||
import { createNotivue } from 'notivue'
|
||||
import 'notivue/notification.css'
|
||||
import 'notivue/animations.css'
|
||||
const notivue = createNotivue({
|
||||
position: 'bottom-center',
|
||||
limit: 4,
|
||||
enqueue: false,
|
||||
avoidDuplicates: true,
|
||||
notifications: {
|
||||
global: {
|
||||
duration: 3000
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const app = createApp(App)
|
||||
app.provide('loading', loading)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.use(notivue)
|
||||
.component('DatePicker', Vue3PersianDatetimePicker)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,57 @@
|
||||
import axios from 'axios'
|
||||
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
||||
|
||||
axios.defaults.baseURL = "./"
|
||||
const pendingRequests = new Map()
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
// Generate a unique key for the request
|
||||
const requestKey = `${config.method}:${config.url}`
|
||||
|
||||
// Check if there is already a pending request with the same key
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
const cancelSource = pendingRequests.get(requestKey)
|
||||
cancelSource.cancel('Duplicate request cancelled')
|
||||
}
|
||||
|
||||
// Create a new cancel token for the request
|
||||
const cancelSource = axios.CancelToken.source()
|
||||
config.cancelToken = cancelSource.token
|
||||
|
||||
// Store the cancel token in the pending requests map
|
||||
pendingRequests.set(requestKey, cancelSource)
|
||||
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data'
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
// Remove the request from the pending requests map
|
||||
const requestKey = `${response.config.method}:${response.config.url}`
|
||||
pendingRequests.delete(requestKey)
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (axios.isCancel(error)) {
|
||||
// Handle duplicate request cancellation here if needed
|
||||
console.warn(error.message)
|
||||
} else {
|
||||
// Remove the request from the pending requests map on error
|
||||
const requestKey = `${error.config.method}:${error.config.url}`
|
||||
pendingRequests.delete(requestKey)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
const api = axios.create()
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,88 @@
|
||||
import api from './api'
|
||||
import { i18n } from '@/locales'
|
||||
import router from '@/router'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export interface Msg {
|
||||
success: boolean
|
||||
msg: string
|
||||
obj: any | null
|
||||
}
|
||||
|
||||
function _handleMsg(msg: any): void {
|
||||
if (!isMsg(msg)) {
|
||||
return
|
||||
}
|
||||
if(msg.msg){
|
||||
if (!msg.success && msg.msg == "Invalid login") {
|
||||
push.error({
|
||||
title: i18n.global.t('invalidLogin'),
|
||||
})
|
||||
logout()
|
||||
return
|
||||
}
|
||||
if (msg.success) {
|
||||
push.success({
|
||||
message: i18n.global.t('success') + ": " + i18n.global.t('actions.' + msg.msg),
|
||||
})
|
||||
} else {
|
||||
push.error({
|
||||
title: i18n.global.t('failed'),
|
||||
message: msg.msg
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logout = async () => {
|
||||
const response = await HttpUtils.get('api/logout')
|
||||
if(response.success){
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
function _respToMsg(resp: any): Msg {
|
||||
const data = resp.data
|
||||
if (data == null) {
|
||||
return { success: true, msg: "", obj: null }
|
||||
} else if (isMsg(data)) {
|
||||
if (data.hasOwnProperty('success')) {
|
||||
return { success: data.success, msg: data.msg, obj: data.obj || null }
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
} else {
|
||||
return { success: false, msg: `unknown data: ${data}`, obj: null }
|
||||
}
|
||||
}
|
||||
|
||||
function isMsg(obj: any): obj is Msg {
|
||||
return Object.hasOwn(obj,'success') && Object.hasOwn(obj,'msg') && Object.hasOwn(obj, 'obj')
|
||||
}
|
||||
|
||||
const HttpUtils = {
|
||||
async get(url: string, data: object = {}, options: any[] = []): Promise<Msg> {
|
||||
let msg: Msg
|
||||
try {
|
||||
const resp = await api.get(url, { params: data, ...options })
|
||||
msg = _respToMsg(resp)
|
||||
} catch (e: any) {
|
||||
msg = { success: false, msg: e.toString(), obj: null }
|
||||
}
|
||||
_handleMsg(msg)
|
||||
return msg
|
||||
},
|
||||
async post(url: string, data: object | null, options: any = undefined): Promise<Msg> {
|
||||
let msg: Msg
|
||||
try {
|
||||
const resp = await api.post(url, data, options)
|
||||
msg = _respToMsg(resp)
|
||||
} catch (e: any) {
|
||||
msg = { success: false, msg: e.toString(), obj: null }
|
||||
}
|
||||
_handleMsg(msg)
|
||||
return msg
|
||||
},
|
||||
}
|
||||
|
||||
export default HttpUtils
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user