mirror of
https://github.com/v2rayA/v2rayA.git
synced 2024-11-25 16:34:19 +08:00
refactor: web gui by nuxt (#787)
This commit is contained in:
parent
5e6a72de43
commit
a91120826a
9
ngui/.gitignore
vendored
Normal file
9
ngui/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
*.log*
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.env
|
||||
.eslintcache
|
||||
dist
|
1
ngui/.npmrc
Normal file
1
ngui/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
shamefully-hoist=true
|
15
ngui/app.vue
Normal file
15
ngui/app.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
|
||||
.el-table .el-table__cell{
|
||||
z-index: unset!important;
|
||||
}
|
||||
</style>
|
22
ngui/components/Header.vue
Normal file
22
ngui/components/Header.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="p-6 flex justify-between">
|
||||
<div class="flex">
|
||||
<h1 class="text-large font-600 mr-3">
|
||||
<NuxtLink to="/">V2RayA</NuxtLink>
|
||||
</h1>
|
||||
|
||||
<OperateBoot />
|
||||
<OperateOutbound />
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<NuxtLink to="/setting">{{ $t('common.setting') }}</NuxtLink>
|
||||
<NuxtLink to="/log">{{ $t("common.log") }}</NuxtLink>
|
||||
<NuxtLink to="/about">{{ $t("common.about") }}</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
15
ngui/components/Operate/Boot.vue
Normal file
15
ngui/components/Operate/Boot.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
const bootV2rayA = async() => {
|
||||
const { data } = await useV2Fetch('v2ray').post().json()
|
||||
if (data.value.code === 'SUCCESS') {
|
||||
system.value.running = true
|
||||
system.value.connect = data.value.data.touch.connectedServer
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton size="small" @click="bootV2rayA">
|
||||
{{ system.running ? $t('common.isRunning') : $t('common.notRunning') }}
|
||||
</ElButton>
|
||||
</template>
|
24
ngui/components/Operate/Connect.vue
Normal file
24
ngui/components/Operate/Connect.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
const { data: row, subID } = defineProps<{ data: any, subID?: number }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const connectServer = async() => {
|
||||
const { data } = await useV2Fetch('connection').post({
|
||||
sub: subID! - 1,
|
||||
id: row.id,
|
||||
_type: row._type,
|
||||
outbound: proxies.value.currentOutbound
|
||||
}).json()
|
||||
|
||||
if (data.value.code === 'SUCCESS')
|
||||
ElMessage.success(t('common.success'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton size="small" @click="connectServer">
|
||||
<UnoIcon class="ri:links-fill mr-1" />
|
||||
{{ row.connected ? $t("operations.cancel") : $t("operations.select") }}
|
||||
</ElButton>
|
||||
</template>
|
41
ngui/components/Operate/Import.vue
Normal file
41
ngui/components/Operate/Import.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n()
|
||||
|
||||
const input = $ref('')
|
||||
|
||||
let isVisible = $ref(false)
|
||||
let isImporting = $ref(false)
|
||||
const ifMulite = $ref(false)
|
||||
|
||||
const handleClickImportConfirm = async() => {
|
||||
isImporting = true
|
||||
const { data } = await useV2Fetch('import').post({ url: input }).json()
|
||||
isImporting = false
|
||||
|
||||
if (data.value.code === 'SUCCESS') {
|
||||
proxies.value.subs = data.value.data.touch.subscriptions
|
||||
ElMessage.success(t('common.success'))
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton @click="isVisible = true">
|
||||
{{ t("operations.import") }}
|
||||
</ElButton>
|
||||
|
||||
<ElDialog v-model="isVisible" :title="$t('operations.import')">
|
||||
{{ $t('import.message') }}
|
||||
<ElInput v-model="input" :type="ifMulite ? 'textarea' : 'url'" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="ifMulite = true">{{ $t('operations.inBatch') }}</ElButton>
|
||||
<ElButton @click="isVisible = false">{{ $t('operations.cancel') }}</ElButton>
|
||||
<ElButton type="primary" :loading="isImporting" @click="handleClickImportConfirm">
|
||||
{{ $t("operations.confirm") }}
|
||||
</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
15
ngui/components/Operate/Latency.vue
Normal file
15
ngui/components/Operate/Latency.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
const { data: row, type } = defineProps<{ data: any, type: 'ping' | 'http' }>()
|
||||
|
||||
const testServer = async(row: any) => {
|
||||
const alias = `${type}Latency`
|
||||
|
||||
const { data } = await useV2Fetch(`${alias}?whiches=${JSON.stringify(row)}`).get().json()
|
||||
for (const i of data.value.data.whiches)
|
||||
proxies.value.subs[i.sub].servers[i.id - 1][alias] = i[alias]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton @click="testServer(row)">{{ type.toUpperCase() }}</ElButton>
|
||||
</template>
|
62
ngui/components/Operate/Outbound.vue
Normal file
62
ngui/components/Operate/Outbound.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n()
|
||||
|
||||
let isVisible = $ref(false)
|
||||
let currentOutbound = $ref('')
|
||||
let setting = $ref<{
|
||||
probeInterval: string
|
||||
probeURL: string
|
||||
type: string
|
||||
}>()
|
||||
|
||||
const viewOutbound = async(outbound: string) => {
|
||||
isVisible = true
|
||||
currentOutbound = outbound
|
||||
const { data } = await useV2Fetch(`outbound?outbound=${outbound}`).json()
|
||||
|
||||
setting = data.value.data.setting
|
||||
}
|
||||
|
||||
const deleteOutbound = async(outbound: string) => {
|
||||
const { data } = await useV2Fetch('outbound').delete({ outbound }).json()
|
||||
proxies.value.outbounds = data.value.data.outbounds
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
const editOutbound = async(outbound: string) => {
|
||||
const { data } = await useV2Fetch('outbound').put({ outbound, setting }).json()
|
||||
if (data.value.code === 'SUCCESS')
|
||||
ElMessage.success(t('common.success'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown class="ml-2">
|
||||
<ElButton size="small">{{ proxies.currentOutbound.toUpperCase() }}</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu class="w-28">
|
||||
<ElDropdownItem v-for="i in proxies.outbounds" :key="i" class="flex justify-between">
|
||||
<div class="w-full" @click="proxies.currentOutbound = i">{{ i }}</div>
|
||||
<UnoIcon class="ri:settings-fill ml-2" @click="viewOutbound(i)" />
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
|
||||
<ElDialog v-model="isVisible" :title="`${currentOutbound}- ${$t('common.outboundSetting')}`">
|
||||
<ElForm>
|
||||
<ElFormItem v-for="(v, k) in setting" :key="k" v-model="setting" :label="k.toString()">
|
||||
<ElInput v-model="setting![k]" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="deleteOutbound(currentOutbound)">{{ $t('operations.delete') }}</ElButton>
|
||||
<ElButton @click="isVisible = false">{{ $t('operations.cancel') }}</ElButton>
|
||||
<ElButton type="primary" @click="editOutbound(currentOutbound)">
|
||||
{{ $t("operations.confirm") }}
|
||||
</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
37
ngui/components/Operate/Remark.vue
Normal file
37
ngui/components/Operate/Remark.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
const { data: row } = defineProps<{ data: any }>()
|
||||
|
||||
const input = $ref('')
|
||||
let isVisible = $ref(false)
|
||||
|
||||
const remarkSubscription = async(remark: string) => {
|
||||
const { data } = await useV2Fetch('subscription').patch({
|
||||
subscription: {
|
||||
...row,
|
||||
remarks: remark
|
||||
}
|
||||
}).json()
|
||||
|
||||
proxies.value.subs = data.value.data.touch.subscriptions
|
||||
isVisible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton size="small" class="mr-3" @click="isVisible = true">
|
||||
<UnoIcon class="ri:edit-2-line mr-1" />{{ $t('operations.modify') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDialog v-model="isVisible" :title="$t('operations.import')">
|
||||
{{ $t("configureSubscription.title") }}
|
||||
<ElInput v-model="input" :placeholder="$t('subscription.remarks')" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="isVisible = false">{{ $t('operations.cancel') }}</ElButton>
|
||||
<ElButton type="primary" @click="remarkSubscription(input)">
|
||||
{{ $t("operations.confirm") }}
|
||||
</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
35
ngui/components/Operate/Share.vue
Normal file
35
ngui/components/Operate/Share.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { useQRCode } from '@vueuse/integrations/useQRCode'
|
||||
const { data, subID } = defineProps<{ data: any, subID?: number }>()
|
||||
|
||||
const { $index, row } = data
|
||||
|
||||
let isVisible = $ref(false)
|
||||
let qrcode = ref('')
|
||||
|
||||
const shareSubscription = async() => {
|
||||
const params = JSON.stringify({
|
||||
id: row.id,
|
||||
_type: row._type,
|
||||
sub: row._type === 'subscription' ? $index : subID! - 1
|
||||
})
|
||||
|
||||
const { data } = await useV2Fetch(`sharingAddress?touch=${params}`).get().json()
|
||||
|
||||
qrcode = useQRCode(data.value.data.sharingAddress)
|
||||
isVisible = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton size="small" @click="shareSubscription">
|
||||
<UnoIcon class="ri:share-fill mr-1" /> {{ $t('operations.share') }}
|
||||
</ElButton>
|
||||
<ElDialog v-model="isVisible" :title="$t('operations.import')" width="250">
|
||||
<template #header="{ titleClass }">
|
||||
<div :class="titleClass">{{ row._type.toUpperCase() }}</div>
|
||||
</template>
|
||||
|
||||
<img :src="qrcode">
|
||||
</ElDialog>
|
||||
</template>
|
146
ngui/components/Operate/View.vue
Normal file
146
ngui/components/Operate/View.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<script lang="ts" setup>
|
||||
import { parseURL } from 'ufo'
|
||||
const { data: row, subID } = defineProps<{ data: any, subID: number }>()
|
||||
|
||||
let isVisible = $ref(false)
|
||||
let serverInfo = $ref<any>()
|
||||
|
||||
const viewServer = async() => {
|
||||
isVisible = true
|
||||
const params = JSON.stringify({
|
||||
id: row.id,
|
||||
_type: row._type,
|
||||
sub: subID! - 1
|
||||
})
|
||||
|
||||
const { data } = await useV2Fetch(`sharingAddress?touch=${params}`).get().json()
|
||||
|
||||
/* ss://BASE64(method:password)@server:port#name */
|
||||
/* ssr://server:port:proto:method:obfs:URLBASE64(password)/?remarks=URLBASE64(remarks)&protoparam=URLBASE64(protoparam)&obfsparam=URLBASE64(obfsparam)) */
|
||||
/* trojan://password@server:port?allowInsecure=1&sni=sni#URIESCAPE(name) */
|
||||
|
||||
serverInfo = parseURL(data.value.data.sharingAddress)
|
||||
|
||||
serverInfo = {
|
||||
...serverInfo,
|
||||
protocol: serverInfo.protocol.slice(0, -1),
|
||||
name: decodeURIComponent(serverInfo.hash).slice(1)
|
||||
}
|
||||
|
||||
switch (serverInfo.protocol) {
|
||||
case 'ss': {
|
||||
const auth = atob(serverInfo.auth).split(':')
|
||||
const address = serverInfo.host.split(':')
|
||||
|
||||
serverInfo = {
|
||||
...serverInfo,
|
||||
host: address[0],
|
||||
port: address[1],
|
||||
method: auth[0],
|
||||
password: auth[1]
|
||||
}
|
||||
|
||||
delete serverInfo.auth
|
||||
break
|
||||
}
|
||||
case 'trojan': {
|
||||
const address = serverInfo.host.split(':')
|
||||
|
||||
serverInfo = {
|
||||
...serverInfo,
|
||||
host: address[0],
|
||||
port: address[1],
|
||||
password: serverInfo.auth,
|
||||
name: decodeURIComponent(serverInfo.hash).slice(1)
|
||||
}
|
||||
|
||||
delete serverInfo.auth
|
||||
break
|
||||
}
|
||||
case 'ssr': {
|
||||
const auth = atob(serverInfo.auth).split(':')
|
||||
const address = serverInfo.host.split(':')
|
||||
|
||||
serverInfo = {
|
||||
...serverInfo,
|
||||
host: address[0],
|
||||
port: address[1],
|
||||
method: auth[0],
|
||||
password: auth[1],
|
||||
protocol: serverInfo.protocol,
|
||||
obfs: serverInfo.obfs,
|
||||
name: decodeURIComponent(serverInfo.hash).slice(1)
|
||||
}
|
||||
|
||||
delete serverInfo.auth
|
||||
break
|
||||
}
|
||||
case 'vless': {
|
||||
const auth = atob(serverInfo.auth).split(':')
|
||||
const address = serverInfo.host.split(':')
|
||||
|
||||
serverInfo = {
|
||||
...serverInfo,
|
||||
host: address[0],
|
||||
port: address[1],
|
||||
method: auth[0],
|
||||
password: auth[1],
|
||||
name: decodeURIComponent(serverInfo.hash).slice(1)
|
||||
}
|
||||
|
||||
delete serverInfo.auth
|
||||
break
|
||||
}
|
||||
case 'vmess': {
|
||||
const parsed: {
|
||||
ps: string
|
||||
add: string
|
||||
port: string
|
||||
id: string
|
||||
aid: string
|
||||
scy: string
|
||||
net: string
|
||||
type: string
|
||||
host: string
|
||||
sni: string
|
||||
path: string
|
||||
tls: string
|
||||
allowInsecure: boolean
|
||||
v: boolean
|
||||
protocol: string
|
||||
} = JSON.parse(atob(serverInfo.host))
|
||||
serverInfo = {
|
||||
...serverInfo,
|
||||
name: parsed.ps,
|
||||
...parsed
|
||||
}
|
||||
|
||||
delete serverInfo.host
|
||||
delete serverInfo.ps
|
||||
break
|
||||
}
|
||||
default: break
|
||||
}
|
||||
|
||||
delete serverInfo.hash
|
||||
Object.keys(serverInfo).forEach((key) => { if (serverInfo[key] === '') delete serverInfo[key] })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton size="small" class="mr-3" @click="viewServer">
|
||||
<UnoIcon class="ri:file-code-line mr-1" />{{ $t("operations.view") }}
|
||||
</ElButton>
|
||||
|
||||
<ElDialog v-model="isVisible">
|
||||
<ElForm>
|
||||
<ElFormItem
|
||||
v-for="(v, k) in serverInfo"
|
||||
:key="k"
|
||||
:label="k.toString()"
|
||||
>
|
||||
<ElInput :value="v" disabled />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElDialog>
|
||||
</template>
|
18
ngui/components/Server.vue
Normal file
18
ngui/components/Server.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n()
|
||||
|
||||
const columns = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'name', label: t('server.name') },
|
||||
{ key: 'address', label: t('server.address') },
|
||||
{ key: 'net', label: t('server.protocol') },
|
||||
{ key: 'pingLatency', label: t('server.latency') }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElTable :data="proxies.servers">
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn v-for="c in columns" :key="c.key" :prop="c.key" :label="c.label" />
|
||||
</ElTable>
|
||||
</template>
|
33
ngui/components/SubServer.vue
Normal file
33
ngui/components/SubServer.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n()
|
||||
|
||||
const { data, id } = defineProps<{ data: any[], id: number }>()
|
||||
|
||||
const columns = [
|
||||
{ key: 'id', label: 'ID', width: 70 },
|
||||
{ key: 'name', label: t('server.name') },
|
||||
{ key: 'address', label: t('server.address') },
|
||||
{ key: 'net', label: t('server.protocol') },
|
||||
{ key: 'pingLatency', label: t('server.latency') }
|
||||
]
|
||||
|
||||
let selectRows = $ref<any[]>([])
|
||||
const handleSelectionChange = (val: any) => { selectRows = val }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OperateLatency :data="selectRows" type="ping" />
|
||||
<OperateLatency :data="selectRows" type="http" />
|
||||
|
||||
<ElTable :data="data" @selection-change="handleSelectionChange">
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn v-for="c in columns" :key="c.key" :prop="c.key" :label="c.label" :width="c.width" />
|
||||
<ElTableColumn :label="t('operations.name')" min-width="240">
|
||||
<template #default="scope">
|
||||
<OperateConnect :data="scope.row" :sub-i-d="id" />
|
||||
<OperateView :data="scope.row" :sub-i-d="id" />
|
||||
<OperateShare :data="scope" :sub-i-d="id" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
59
ngui/components/Subscription.vue
Normal file
59
ngui/components/Subscription.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n()
|
||||
|
||||
const columns = [
|
||||
{ key: 'id', label: 'ID', width: 70 },
|
||||
{ key: 'host', label: t('subscription.host'), width: 220 },
|
||||
{ key: 'remarks', label: t('subscription.remarks'), width: 120 },
|
||||
{ key: 'status', label: t('subscription.timeLastUpdate'), width: 180 }
|
||||
]
|
||||
|
||||
let selectRows = $ref<any[]>([])
|
||||
const handleSelectionChange = (val: any) => { selectRows = val }
|
||||
|
||||
let isUpdating = $ref(false)
|
||||
const updateSubscription = async(row: any) => {
|
||||
isUpdating = true
|
||||
const { data } = await useV2Fetch('subscription').put({ id: row.id, _type: row._type }).json()
|
||||
isUpdating = false
|
||||
|
||||
if (data.value.code === 'SUCCESS')
|
||||
ElMessage.success({ message: t('common.success'), duration: 5000 })
|
||||
}
|
||||
|
||||
const removeSubscription = async(row: any) => {
|
||||
const { data } = await useV2Fetch('touch').delete({
|
||||
touches: selectRows.map((x) => { return { id: x.id, _type: x._type } })
|
||||
}).json()
|
||||
|
||||
if (data.value.code === 'SUCCESS')
|
||||
proxies.value.subs = data.value.data.touch.subscriptions
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OperateImport />
|
||||
|
||||
<ElButton
|
||||
v-if="!(Array.isArray(selectRows) && selectRows.length === 0)"
|
||||
class="ml-2"
|
||||
@click="removeSubscription(selectRows)"
|
||||
>
|
||||
{{ t('operations.delete') }}
|
||||
</ElButton>
|
||||
|
||||
<ElTable :data="proxies.subs" @selection-change="handleSelectionChange">
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn v-for="c in columns" :key="c.key" :prop="c.key" :label="c.label" :min-width="c.width" />
|
||||
<ElTableColumn property="servers.length" :label="t('subscription.numberServers')" min-width="70" />
|
||||
<ElTableColumn :label="t('operations.name')" min-width="240">
|
||||
<template #default="scope">
|
||||
<ElButton size="small" :loading="isUpdating" @click="updateSubscription(scope.row)">
|
||||
<UnoIcon v-if="!isUpdating" class="ri:refresh-line mr-1" /> {{ t('operations.update') }}
|
||||
</ElButton>
|
||||
<OperateRemark :data="scope.row" />
|
||||
<OperateShare :data="scope" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
33
ngui/composables/fetch.ts
Normal file
33
ngui/composables/fetch.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createFetch } from '@vueuse/core'
|
||||
|
||||
export const useV2Fetch = createFetch({
|
||||
baseUrl: `${system.value.api}/api/`,
|
||||
combination: 'overwrite',
|
||||
options: {
|
||||
beforeFetch({ options }) {
|
||||
if (user.value.token) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
'Authorization': user.value.token,
|
||||
'X-V2raya-Request-Id': nanoid()
|
||||
}
|
||||
}
|
||||
|
||||
return { options }
|
||||
},
|
||||
afterFetch({ data }) {
|
||||
if (data.code === 'FAIL')
|
||||
ElMessage.error({ message: data?.message })
|
||||
|
||||
return { data }
|
||||
},
|
||||
onFetchError({ error }) {
|
||||
if (error)
|
||||
ElMessage.error({ message: error?.message })
|
||||
|
||||
return { error }
|
||||
}
|
||||
}
|
||||
})
|
12
ngui/composables/proxies.ts
Normal file
12
ngui/composables/proxies.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const proxies
|
||||
= useLocalStorage<{
|
||||
currentOutbound: string
|
||||
outbounds: any[]
|
||||
subs: any[]
|
||||
servers: any[]
|
||||
}>('proxies', {
|
||||
currentOutbound: 'proxy',
|
||||
outbounds: [],
|
||||
subs: [],
|
||||
servers: []
|
||||
})
|
9
ngui/composables/system.ts
Normal file
9
ngui/composables/system.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const system = useLocalStorage('system', {
|
||||
api: 'http://127.0.0.1:2017',
|
||||
running: false,
|
||||
connect: '',
|
||||
docker: false,
|
||||
version: '',
|
||||
lite: 'false',
|
||||
gfwlist: ''
|
||||
})
|
5
ngui/composables/user.ts
Normal file
5
ngui/composables/user.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const user = useLocalStorage('user', {
|
||||
token: '',
|
||||
firstCheck: true,
|
||||
exist: false
|
||||
})
|
6
ngui/layouts/default.vue
Normal file
6
ngui/layouts/default.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<Header />
|
||||
<div class="mx-6">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
294
ngui/locales/zh-hans.yaml
Normal file
294
ngui/locales/zh-hans.yaml
Normal file
@ -0,0 +1,294 @@
|
||||
common:
|
||||
outboundSetting: 出站设置
|
||||
setting: 设置
|
||||
about: 关于
|
||||
loggedAs: '正在以 <b>{username}</b> 的身份登录'
|
||||
v2rayCoreStatus: v2ray-core状态
|
||||
checkRunning: 检测中
|
||||
isRunning: 正在运行
|
||||
notRunning: 就绪
|
||||
notLogin: 未登录
|
||||
latest: 最新
|
||||
local: 本地
|
||||
success: 成功
|
||||
fail: 失败
|
||||
message: 提示
|
||||
none: 无
|
||||
optional: 可选
|
||||
loadBalance: 负载均衡
|
||||
log: 日志
|
||||
welcome:
|
||||
title: 初来乍到,请多关照
|
||||
docker: 'v2rayA服务端正在运行于Docker环境中,Version: {version}'
|
||||
default: 'v2rayA服务端正在运行,Version: {version}'
|
||||
newVersion: '检测到新版本: {version}'
|
||||
messages:
|
||||
- 我们发现你还没有创建或导入任何节点、订阅。
|
||||
- 我们支持以节点地址或者订阅地址的方式导入,也支持手动创建节点,快来试试吧!
|
||||
v2ray:
|
||||
start: 启动
|
||||
stop: 关闭
|
||||
server:
|
||||
name: 节点名
|
||||
address: 节点地址
|
||||
protocol: 协议
|
||||
latency: 时延
|
||||
lastSeenTime: 上次存活时间
|
||||
lastTryTime: 上次测试时间
|
||||
messages:
|
||||
notAllowInsecure: '根据 {name} 的官方文档,如果你使用 {name},将不允许 AllowInsecure'
|
||||
notRecommend: '根据 {name} 的官方文档,如果你使用 {name},不推荐开启 AllowInsecure'
|
||||
InSecureConfirm:
|
||||
title: 检测到不安全的配置
|
||||
message: 即将保存的配置中<b>AllowInsecure</b>被设置为true,除非你知道你在做什么,否则贸然开启可能导致数据泄漏!是否继续?
|
||||
confirm: 我知道我在做什么
|
||||
cancel: 取消
|
||||
subscription:
|
||||
host: 域名
|
||||
remarks: 别名
|
||||
timeLastUpdate: 上次更新时间
|
||||
numberServers: 节点数
|
||||
operations:
|
||||
name: 操作
|
||||
update: 更新
|
||||
modify: 修改
|
||||
share: 分享
|
||||
view: 查看
|
||||
delete: 删除
|
||||
create: 创建
|
||||
import: 导入
|
||||
inBatch: 批量
|
||||
connect: 连接
|
||||
disconnect: 断开
|
||||
select: 选择
|
||||
login: 登录
|
||||
logout: 注销
|
||||
configure: 配置
|
||||
cancel: 取消
|
||||
confirm: 确定
|
||||
confirm2: 确认无误
|
||||
saveApply: 保存并应用
|
||||
save: 保存
|
||||
copyLink: 复制链接
|
||||
helpManual: 查看帮助
|
||||
'yes': 是
|
||||
'no': 否
|
||||
switchSite: 切换至备用站点
|
||||
addOutbound: 新增一个出站 (outbound)
|
||||
register:
|
||||
title: 初来乍到,创建一个管理员账号
|
||||
messages:
|
||||
- 请记住您创建的管理员账号,用于登录该管理页面。
|
||||
- 账号信息位于本地,我们不会上传任何信息到服务器。
|
||||
- 如不慎忘记密码,使用 v2raya --reset-password 重置。
|
||||
login:
|
||||
title: 登录
|
||||
username: 用户名
|
||||
password: 密码
|
||||
setting:
|
||||
transparentProxy: 透明代理/系统代理
|
||||
transparentType: 透明代理/系统代理实现方式
|
||||
pacMode: 规则端口的分流模式
|
||||
preventDnsSpoofing: 防止DNS污染
|
||||
specialMode: 特殊模式
|
||||
mux: 多路复用
|
||||
autoUpdateSub: 自动更新订阅
|
||||
autoUpdateGfwlist: 自动更新GFWList
|
||||
preferModeWhenUpdate: 解析订阅链接/更新时优先使用
|
||||
ipForwardOn: 开启IP转发
|
||||
portSharingOn: 开启端口分享
|
||||
concurrency: 最大并发数
|
||||
options:
|
||||
global: 不进行分流
|
||||
direct: 直连模式
|
||||
pac: 跟随规则端口
|
||||
whitelistCn: 大陆白名单模式
|
||||
gfwlist: GFWList模式
|
||||
sameAsPacMode: 分流规则与规则端口所选模式一致
|
||||
customRouting: 自定义路由规则
|
||||
antiDnsHijack: 仅防止DNS劫持(快速)
|
||||
forwardDnsRequest: 转发DNS请求
|
||||
doh: DoH(DNS-over-HTTPS)
|
||||
default: 保持系统默认
|
||||
'on': 启用
|
||||
'off': 关闭
|
||||
updateSubWhenStart: 服务端启动时更新订阅
|
||||
updateSubAtIntervals: 每隔一段时间更新订阅(单位:小时)
|
||||
updateGfwlistWhenStart: 服务端启动时更新GFWList
|
||||
updateGfwlistAtIntervals: 每隔一段时间更新GFWList(单位:小时)
|
||||
dependTransparentMode: 跟随透明代理/系统代理
|
||||
closed: 关闭
|
||||
advanced: 自定义高级设置
|
||||
leastPing: 最小时延优先
|
||||
messages:
|
||||
gfwlist: 该时间是指本地文件最后修改时间,因此可能会领先最新版本
|
||||
transparentProxy: >-
|
||||
全局代理开启后,无需经过额外设置,任何TCP流量均会经过V2RayA。另外,如需作为网关使得连接本机的其他主机或docker也享受代理,请勾选“开启局域网共享”。
|
||||
transparentType: '★tproxy: 支持udp,不支持docker。★redirect: docker友好,不支持udp,需要占用本地53端口以应对dns污染。'
|
||||
pacMode: 该选项设置规则分流端口所使用的路由模式。默认情况下规则分流端口为20172,HTTP协议。
|
||||
preventDnsSpoofing: '★转发DNS查询: 通过代理服务器转发DNS请求。★DoH(v2ray-core: 4.22.0+): DNS over HTTPS。'
|
||||
specialMode: >-
|
||||
★supervisor:监控dns污染,提前拦截,利用v2ray-core的sniffing解决污染。★fakedns:使用fakedns策略加速解析。
|
||||
tcpFastOpen: 简化TCP握手流程以加速建立连接,可能会增加封包的特征。若系统不支持可能会导致无法正常连接。
|
||||
mux: 复用TCP连接以减少握手次数,但会影响吞吐量大的使用场景,如观看视频、下载、测速。当前仅支持vmess节点。可能会增加特征造成断流。
|
||||
confirmEgressPorts: |-
|
||||
<p>您正在对不同子网下的机器设置透明代理,请确认不走代理的出方向端口。</p>
|
||||
<p>当前设置的端口白名单为:</p>
|
||||
<p>TCP: {tcpPorts}</p>
|
||||
<p>UDP: {udpPorts}</p>
|
||||
grpcShouldWithTls: gRPC必须启用TLS
|
||||
ssPluginImpl: >-
|
||||
★默认:使用 simple-obfs 时为等效传输层,v2ray-plugin 时为链式。★链式:shadowsocks
|
||||
流量会被转发至独立的插件。★等效传输层:直接由 v2ray/xray 核心的传输层处理。
|
||||
customAddressPort:
|
||||
title: 地址与端口
|
||||
serviceAddress: 服务端地址
|
||||
portSocks5: socks5端口
|
||||
portHttp: http端口
|
||||
portSocks5WithPac: socks5端口(带分流规则)
|
||||
portHttpWithPac: http端口(带分流规则)
|
||||
portVmess: VMess端口(带分流规则)
|
||||
portVmessLink: VMess端口链接
|
||||
messages:
|
||||
- >-
|
||||
如需修改后端运行地址(默认0.0.0.0:2017),可添加环境变量<code>V2RAYA_ADDRESS</code>或添加启动参数<code>--address</code>。
|
||||
- >-
|
||||
docker模式下如果未使用<code>--privileged --network
|
||||
host</code>参数启动容器,可通过修改端口映射修改socks5、http端口。
|
||||
- docker模式下不能正确判断端口占用,请确保输入的端口未被其他程序占用。
|
||||
- 如将端口设为0则表示关闭该端口。
|
||||
customRouting:
|
||||
title: 自定义路由规则
|
||||
defaultRoutingRule: 默认路由规则
|
||||
sameAsDefaultRule: 与默认规则相同
|
||||
appendRule: 追加规则
|
||||
direct: 直连
|
||||
proxy: 代理
|
||||
block: 拦截
|
||||
rule: 规则
|
||||
domainFile: 域名文件
|
||||
typeRule: 规则类型
|
||||
messages:
|
||||
'0': '将SiteDat文件放于 <b>{V2RayLocationAsset}</b> 目录下,V2rayA将自动进行识别'
|
||||
'1': >-
|
||||
制作SiteDat文件:<a
|
||||
href="https://github.com/ToutyRater/V2Ray-SiteDAT">ToutyRater/V2Ray-SiteDAT</a>
|
||||
'2': 在选择Tags时,可按Ctrl等多选键进行多选。
|
||||
noSiteDatFileFound: '未在{V2RayLocationAsset}中发现siteDat文件'
|
||||
emptyRuleNotPermitted: 不能存在tags为空的规则,请检查
|
||||
doh:
|
||||
title: 配置DoH服务器
|
||||
dohPriorityList: DoH服务优先级列表
|
||||
messages:
|
||||
- >-
|
||||
DoH即DNS over
|
||||
HTTPS,能够有效避免DNS污染,一些国内的DoH提供商自己本身也有被污染的可能,另外,一些DoH提供商的DoH服务可能被墙,请自行选择非代理条件下直连速度较快且效果较好的DoH提供商
|
||||
- '大陆较好的DoH服务有阿里dns, geekdns, rubyfish等'
|
||||
- '台湾有quad101: dns.twnic.tw等'
|
||||
- '美国有cloudflare: 1.0.0.1等'
|
||||
- >-
|
||||
清单:<a href="https://dnscrypt.info/public-servers"
|
||||
target="_blank">public-servers</a>
|
||||
- >-
|
||||
另外,您可以在自己的国内服务器上自架DoH服务,以纵享丝滑 <a
|
||||
href="https://github.com/facebookexperimental/doh-proxy"
|
||||
target="_blank">doh-proxy</a>。在这种情况下,建议同时运行服务端(doh-proxy/doh-httpproxy,
|
||||
提供对外服务)和客户端(doh-stub,
|
||||
连接至doh.opendns.com)并将他们串联,因为通常情况下在普遍受到污染的地区找到一个完全不受污染的服务器是很困难的。
|
||||
- 建议上述列表1-2行即可,留空保存可恢复默认
|
||||
dns:
|
||||
title: 配置DNS服务器
|
||||
internalQueryServers: 域名查询服务器
|
||||
externalQueryServers: 国外域名查询服务器
|
||||
messages:
|
||||
- >-
|
||||
“@:dns.internalQueryServers” 用于查询国内域名,而 “@:dns.externalQueryServers”
|
||||
用于查询国外域名。
|
||||
- >-
|
||||
如果将 “@:dns.externalQueryServers” 留空,“@:dns.internalQueryServers”
|
||||
将会负责查询所有域名。
|
||||
egressPortWhitelist:
|
||||
title: 出方向端口白名单
|
||||
tcpPortWhitelist: TCP端口白名单
|
||||
udpPortWhitelist: UDP端口白名单
|
||||
messages:
|
||||
- 如果你将v2rayA架设在对外提供服务的服务器A上,连接了代理服务器B,那么你需要注意:
|
||||
- >-
|
||||
透明代理会使得所有TCP、UDP流量走代理,通过走代理的流量其源IP地址会被替换为代理服务器B的IP地址,那么如果有客户向你的服务器A发出请求,他却将得到从你代理服务器B发出的回答,该回答在客户看来无疑是不合法的,从而导致连接被拒绝。
|
||||
- '因此,需要将服务器提供的对外服务端口包含在白名单中,使其不走代理。如ssh(22)、v2raya({v2rayaPort})。'
|
||||
- 如不对外提供服务或仅对局域网内主机提供服务,则可不设置白名单。
|
||||
- '格式:22表示端口22,20170:20172表示20170到20172三个端口。'
|
||||
configureServer:
|
||||
title: 配置节点 | 节点
|
||||
servername: 节点名称
|
||||
port: 端口号
|
||||
forceTLS: 强制开启TLS
|
||||
noObfuscation: 不伪装
|
||||
httpObfuscation: 伪装为HTTP
|
||||
srtpObfuscation: 伪装视频通话(SRTP)
|
||||
utpObfuscation: 伪装为BT下载(uTP)
|
||||
wechatVideoObfuscation: 伪装为微信视频通话
|
||||
dtlsObfuscation: 伪装为DTLS1.2数据包
|
||||
wireguardObfuscation: 伪装为WireGuard数据包
|
||||
hostObfuscation: 域名(host)
|
||||
pathObfuscation: 路径(path)
|
||||
seedObfuscation: 混淆种子
|
||||
username: 用户名
|
||||
password: 密码
|
||||
origin: 原版
|
||||
configureSubscription:
|
||||
title: 订阅配置
|
||||
import:
|
||||
message: 填入节点链接或订阅地址:
|
||||
batchMessage: '一行一个节点链接:'
|
||||
qrcodeError: 找不到有效的二维码,请重新尝试
|
||||
delete:
|
||||
title: 确认删除
|
||||
message: 确定要<b>删除</b>这些节点/订阅吗?注意,该操作是不可逆的。
|
||||
latency:
|
||||
message: 时延测试往往需要花费较长时间,请耐心等待
|
||||
version:
|
||||
higherVersionNeeded: '该操作需要 v2rayA 的版本高于{version}'
|
||||
v2rayInvalid: '检测到 geosite.dat, geoip.dat 文件或 v2ray-core 可能未正确安装,请检查'
|
||||
v2rayNotV5: 检测到 v2ray-core 的版本并非 v5,请使用 v5 版本或将 v2rayA 降级至 v1.5
|
||||
about: |-
|
||||
<p>v2rayA 是 V2Ray 的一个 Web 客户端。</p>
|
||||
<p>默认端口:</p>
|
||||
<ul>
|
||||
<ol><code>2017</code>: v2rayA后端端口</ol>
|
||||
<ol><code>20170</code>: SOCKS协议</ol>
|
||||
<ol><code>20171</code>: HTTP协议</ol>
|
||||
<ol><code>20172</code>: 带分流规则的HTTP协议</ol>
|
||||
</ul>
|
||||
<p>其他端口:</p>
|
||||
<ul>
|
||||
<ol><code>32345</code>: tproxy,透明代理所需 </ol>
|
||||
</ul>
|
||||
<p>在使用中如果发现任何问题,欢迎<a href="https://github.com/v2rayA/v2rayA/issues">提出issue</a>.</p>
|
||||
<p>文档:<a href="https://v2raya.org">https://v2raya.org</a>.</p>
|
||||
axios:
|
||||
messages:
|
||||
optimizeBackend: 您是否需要调整服务端地址?
|
||||
noBackendFound: '未在 {url} 检测到v2rayA服务端,请确定v2rayA正常运行'
|
||||
cannotCommunicate:
|
||||
- 无法通信。如果您的服务端已正常运行,且端口正常开放,原因可能是当前浏览器不允许https站点访问http资源,您可以尝试切换为http备用站点。
|
||||
- 无法通信。火狐浏览器不允许https站点访问http资源,您可以换用Chrome浏览器或切换为http备用站点。
|
||||
urls:
|
||||
usage: 'https://v2raya.org/'
|
||||
routingA:
|
||||
messages:
|
||||
- 点击“查看帮助”按钮以获取帮助
|
||||
outbound:
|
||||
addMessage: 请输入你想要添加的出站(outbound)名称:
|
||||
deleteMessage: '确定要<b>删除</b>出站 "{outboundName}" 吗?注意,该操作是不可逆的。'
|
||||
driver:
|
||||
welcome:
|
||||
- 首先导入节点服务器
|
||||
- 初次使用,还没有任何节点服务器,在这里导入或创建节点服务器。
|
||||
tabs:
|
||||
- 订阅与节点服务器
|
||||
- 导入订阅、节点服务器后,在这里切换和管理你的订阅、普通节点以及订阅节点。
|
||||
log:
|
||||
logModalTitle: 查看日志
|
||||
refreshInterval: 刷新间隔
|
||||
seconds: 秒
|
8
ngui/middleware/auth.ts
Normal file
8
ngui/middleware/auth.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export default defineNuxtRouteMiddleware(async() => {
|
||||
if (!user.value.token) {
|
||||
if (user.value.exist)
|
||||
return navigateTo('/login')
|
||||
else
|
||||
return navigateTo('/signup')
|
||||
}
|
||||
})
|
38
ngui/middleware/every-check.ts
Normal file
38
ngui/middleware/every-check.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { MessageParams } from 'element-plus'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async() => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const { t } = nuxtApp.$i18n
|
||||
|
||||
const { data } = await useV2Fetch<any>('version').json()
|
||||
|
||||
if (data.value.code === 'SUCCESS') {
|
||||
system.value.docker = data.value.data.dockerMode
|
||||
system.value.version = data.value.data.version
|
||||
system.value.lite = data.value.data.lite
|
||||
|
||||
let messageConf: MessageParams = {
|
||||
message: t(system.value.docker ? 'welcome.docker' : 'welcome.default', {
|
||||
version: system.value.version
|
||||
}),
|
||||
duration: 3000
|
||||
}
|
||||
|
||||
if (data.value.data.foundNew) {
|
||||
messageConf = {
|
||||
duration: 5000,
|
||||
type: 'success',
|
||||
message: `${messageConf.message}. ${t('welcome.newVersion', {
|
||||
version: data.value.data.remoteVersion
|
||||
})}`
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage(messageConf)
|
||||
|
||||
if (data.value.data.serviceValid === false)
|
||||
ElMessage.error({ message: t('version.v2rayInvalid'), duration: 10000 })
|
||||
else if (!data.value.data.v5)
|
||||
ElMessage.error({ message: t('version.v2rayNotV5'), duration: 10000 })
|
||||
}
|
||||
})
|
8
ngui/middleware/first-check.global.ts
Normal file
8
ngui/middleware/first-check.global.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export default defineNuxtRouteMiddleware(async() => {
|
||||
if (user.value.firstCheck) {
|
||||
// TODO: change backend api
|
||||
const { data } = await useV2Fetch<any>('account').post({ username: '', password: 'aaaaaa' }).json()
|
||||
user.value.firstCheck = false
|
||||
user.value.exist = data.value?.message === 'register closed'
|
||||
}
|
||||
})
|
28
ngui/nuxt.config.ts
Normal file
28
ngui/nuxt.config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
modules: [
|
||||
'@vueuse/nuxt',
|
||||
'@unocss/nuxt',
|
||||
'@nuxtjs/i18n',
|
||||
'@element-plus/nuxt'
|
||||
],
|
||||
i18n: {
|
||||
strategy: 'no_prefix',
|
||||
langDir: 'locales',
|
||||
locales: [
|
||||
{
|
||||
code: 'zh',
|
||||
iso: 'zh-hans',
|
||||
file: 'zh-hans.yaml',
|
||||
name: '简体中文'
|
||||
}
|
||||
]
|
||||
},
|
||||
unocss: {
|
||||
preflight: true
|
||||
},
|
||||
experimental: {
|
||||
reactivityTransform: true
|
||||
}
|
||||
})
|
31
ngui/package.json
Normal file
31
ngui/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"lint": "eslint . --cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/nuxt": "^1.0.3",
|
||||
"@iconify-json/ri": "^1.1.5",
|
||||
"@nuxtjs/i18n": "8.0.0-beta.9",
|
||||
"@unocss/nuxt": "^0.49.4",
|
||||
"@vueuse/core": "^9.12.0",
|
||||
"@vueuse/integrations": "^9.12.0",
|
||||
"@vueuse/nuxt": "^9.12.0",
|
||||
"element-plus": "^2.2.30",
|
||||
"nanoid": "^4.0.1",
|
||||
"nuxt": "3.2.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kecrily/eslint-config": "^0.2.2",
|
||||
"eslint": "^8.34.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@kecrily"
|
||||
}
|
||||
}
|
11
ngui/pages/about.vue
Normal file
11
ngui/pages/about.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="mx-auto w-96">
|
||||
<div class="text-2xl font-bold">mzz2017 / v2rayA</div>
|
||||
<div class="prose" v-html="$t(`about`)" />
|
||||
<a href="https://github.com/v2rayA/v2rayA" target="_blank" class="flex space-x-2">
|
||||
<img src="https://img.shields.io/github/stars/mzz2017/v2rayA.svg?style=social" alt="stars">
|
||||
<img src="https://img.shields.io/github/forks/mzz2017/v2rayA.svg?style=social" alt="forks">
|
||||
<img src="https://img.shields.io/github/watchers/mzz2017/v2rayA.svg?style=social" alt="watchers">
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
34
ngui/pages/index.vue
Normal file
34
ngui/pages/index.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({ middleware: ['auth', 'every-check'] })
|
||||
|
||||
const currentTab = ref('SUBSCRIPTION')
|
||||
|
||||
const { data: { value: { data: { outbounds } } } } = await useV2Fetch<any>('outbounds').json()
|
||||
const { data: { value: { data: { touch } } } } = await useV2Fetch<any>('touch').json()
|
||||
|
||||
proxies.value = {
|
||||
...proxies.value,
|
||||
outbounds,
|
||||
subs: touch.subscriptions,
|
||||
servers: touch.servers
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElTabs v-model="currentTab" class="mb-10" tab-position="left">
|
||||
<ElTabPane name="SUBSCRIPTION" label="SUBSCRIPTION">
|
||||
<Subscription />
|
||||
</ElTabPane>
|
||||
<ElTabPane name="SERVER" label="SERVER">
|
||||
<Server />
|
||||
</ElTabPane>
|
||||
<ElTabPane
|
||||
v-for="s in proxies.subs"
|
||||
:key="`${s.host}:${s.id}`"
|
||||
:name="s.host"
|
||||
:label="s.remarks || s.host.toUpperCase()"
|
||||
>
|
||||
<SubServer :id="s.id" :data="s.servers" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</template>
|
20
ngui/pages/log.vue
Normal file
20
ngui/pages/log.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import { parseURL } from 'ufo'
|
||||
|
||||
definePageMeta({ middleware: ['auth'] })
|
||||
|
||||
const message = $ref<string[]>([])
|
||||
|
||||
const parsed = parseURL(system.value.api)
|
||||
const socket = new WebSocket(`ws://${parsed.host}/api/message?Authorization=${encodeURIComponent(user.value.token)}`)
|
||||
|
||||
socket.onmessage = (msg) => { message.push(msg.data) }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pre class="bg-black text-white rounded-md">
|
||||
<code>
|
||||
{{ Array.isArray(message) && message.length === 0 ? 'Empty' : message }}
|
||||
</code>
|
||||
</pre>
|
||||
</template>
|
54
ngui/pages/login.vue
Normal file
54
ngui/pages/login.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
if (!user.value.exist) navigateTo('/signup')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const username = $ref('')
|
||||
const password = $ref('')
|
||||
|
||||
async function login() {
|
||||
const { data } = await useV2Fetch<any>('login').post({ username, password }).json()
|
||||
|
||||
if (data.value.code !== 'SUCCESS') {
|
||||
ElMessage.warning({ message: data.value.message, duration: 5000 })
|
||||
} else {
|
||||
user.value.token = data.value.data.token
|
||||
ElMessage.success(t('common.success'))
|
||||
navigateTo('/')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto w-96">
|
||||
<h1 class="text-2xl mb-6">{{ `${t('login.title')} - v2rayA` }}</h1>
|
||||
|
||||
<ElForm label-width="auto">
|
||||
<ElFormItem :label="t('login.username')">
|
||||
<ElInput v-model="username" autofocus />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="t('login.password')">
|
||||
<ElInput v-model="password" type="password" show-password />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" class="flex mx-auto" :disabled="username === '' || password === ''" @click="login">
|
||||
{{ t("operations.login") }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert type="info" show-icon :closable="false">
|
||||
If you forget your password, you can reset it by exec <code>v2raya --reset-password</code> and restarting v2rayA.
|
||||
|
||||
<a @click="user.exist = false">Already reset password</a>
|
||||
</ElAlert>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.va-input-wrapper--labeled .va-input-wrapper__label {
|
||||
height: 14px;
|
||||
}
|
||||
</style>
|
200
ngui/pages/setting.vue
Normal file
200
ngui/pages/setting.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({ middleware: ['auth'] })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
let setting = $ref<any>()
|
||||
const { data } = await useV2Fetch<any>('setting').json()
|
||||
|
||||
system.value.gfwlist = data.value.data.localGFWListVersion
|
||||
|
||||
setting = data.value.data.setting
|
||||
|
||||
const { data: { value: { data: { remoteGFWListVersion } } } }
|
||||
= await useV2Fetch<any>('remoteGFWListVersion').json()
|
||||
|
||||
const updateGFWList = async() => {
|
||||
const { data } = await useV2Fetch<any>('gfwList').put().json()
|
||||
if (data.value.code === 'SUCCESS') {
|
||||
system.value.gfwlist = data.value.data.localGFWListVersion
|
||||
|
||||
ElMessage.success(t('common.success'))
|
||||
}
|
||||
}
|
||||
|
||||
const updateSetting = async() => {
|
||||
const { data } = await useV2Fetch<any>('setting').put(setting).json()
|
||||
if (data.value.code === 'SUCCESS')
|
||||
ElMessage.success(t('common.success'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col mx-auto w-108">
|
||||
<div>
|
||||
<div>GFWList</div>
|
||||
<div>{{ $t('common.latest') }}:</div>
|
||||
<ElLink href="https://github.com/v2ray-a/dist-v2ray-rules-dat/releases">
|
||||
{{ remoteGFWListVersion }}
|
||||
</ElLink>
|
||||
|
||||
<div>{{ $t('common.local') }}:</div>
|
||||
<div>{{ system.gfwlist }}</div>
|
||||
<ElButton size="small" @click="updateGFWList">{{ $t('operations.update') }}</ElButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{{ $t('setting.transparentProxy') }}</div>
|
||||
<ElSelect v-model="setting.transparent" size="small">
|
||||
<ElOption value="close" :label="$t('setting.options.off')" />
|
||||
<ElOption value="proxy" :label="`${$t('setting.options.on')}:${$t('setting.options.global')}`" />
|
||||
<ElOption value="whitelist" :label="`${$t('setting.options.on')}:${$t('setting.options.whitelistCn')}`" />
|
||||
<ElOption value="gfwlist" :label="`${$t('setting.options.on')}:${$t('setting.options.gfwlist')}`" />
|
||||
<ElOption value="pac" :label="`${$t('setting.options.on')}:${$t('setting.options.sameAsPacMode')}`" />
|
||||
</ElSelect>
|
||||
|
||||
<ElCheckboxButton v-show="!system.lite" v-model="setting.ipforward">
|
||||
{{ $t('setting.ipForwardOn') }}
|
||||
</ElCheckboxButton>
|
||||
|
||||
<ElCheckboxButton v-model="setting.portSharing">
|
||||
{{ $t('setting.portSharingOn') }}
|
||||
</ElCheckboxButton>
|
||||
</div>
|
||||
|
||||
<div v-if="setting.transparent !== 'close'">
|
||||
<div>{{ $t('setting.transparentType') }}</div>
|
||||
<ElSelect v-model="setting.transparentType" size="small">
|
||||
<ElOption v-show="!system.lite" value="redirect">redirect</ElOption>
|
||||
<ElOption v-show="!system.lite" value="tproxy">tproxy</ElOption>
|
||||
<ElOption value="system_proxy">system proxy</ElOption>
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{{ $t('setting.pacMode') }}</div>
|
||||
<ElSelect v-model="setting.pacMode" size="small">
|
||||
<ElOption value="whitelist" :label="$t('setting.options.whitelistCn')" />
|
||||
<ElOption value="gfwlist" :label="$t('setting.options.gfwlist')" />
|
||||
<!-- <ElOption value="custom" :label="$t('setting.options.customRouting')" /> -->
|
||||
<ElOption value="routingA" label="RoutingA" />
|
||||
</ElSelect>
|
||||
|
||||
<ElButton v-if="setting.pacMode === 'custom'">{{ $t('operations.configure') }}</ElButton>
|
||||
<ElButton v-if="setting.pacMode === 'routingA'">{{ $t('operations.configure') }}</ElButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{{ $t('setting.preventDnsSpoofing') }}</div>
|
||||
<ElSelect v-model="setting.antipollution" size="small">
|
||||
<ElOption value="closed" :label="$t('setting.options.closed')" />
|
||||
<ElOption value="none" :label=" $t('setting.options.antiDnsHijack')" />
|
||||
<ElOption value="dnsforward" :label="$t('setting.options.forwardDnsRequest')" />
|
||||
<ElOption value="doh" :label="$t('setting.options.doh')" />
|
||||
<ElOption value="advanced" :label="$t('setting.options.advanced')" />
|
||||
</ElSelect>
|
||||
|
||||
<ElButton v-if="setting.antipollution === 'advanced'">{{ $t('operations.configure') }}</ElButton>
|
||||
</div>
|
||||
|
||||
<div v-if="setting.showSpecialMode">
|
||||
<div>{{ $t('setting.specialMode') }}</div>
|
||||
<ElSelect v-model="setting.specialMode" size="small">
|
||||
<ElOption value="none" :label="$t('setting.options.closed')" />
|
||||
<ElOption value="supervisor">supervisor</ElOption>
|
||||
<ElOption v-show="setting.antipollution !== 'closed'" value="fakedns">
|
||||
fakedns
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>TCPFastOpen</div>
|
||||
<ElSelect v-model="setting.tcpFastOpen" size="small">
|
||||
<ElOption value="default" :label="$t('setting.options.default')" />
|
||||
<ElOption value="yes" :label="$t('setting.options.on')" />
|
||||
<ElOption value="no" :label="$t('setting.options.off')" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{{ $t('setting.mux') }}</div>
|
||||
<ElSelect v-model="setting.muxOn" size="small">
|
||||
<ElOption value="no" :label="$t('setting.options.off')" />
|
||||
<ElOption value="yes" :label="$t('setting.options.on')" />
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-if="setting.muxOn === 'yes'"
|
||||
ref="muxinput" v-model="setting.mux"
|
||||
:placeholder="$t('setting.concurrency')"
|
||||
type="number" min="1" max="1024"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="setting.pacMode === 'gfwlist' || setting.transparent === 'gfwlist'">
|
||||
<div>{{ $t('setting.options.off') }}</div>
|
||||
<ElSelect v-model="setting.pacAutoUpdateMode" size="small">
|
||||
<ElOption value="none" :label="$t('setting.options.off')" />
|
||||
<ElOption value="auto_update" :label="$t('setting.options.updateGfwlistWhenStart')" />
|
||||
<ElOption value="auto_update_at_intervals" :label="$t('setting.options.updateGfwlistAtIntervals')" />
|
||||
|
||||
<ElInput
|
||||
v-if="setting.pacAutoUpdateMode === 'auto_update_at_intervals'"
|
||||
ref="autoUpdatePacInput"
|
||||
v-model="setting.pacAutoUpdateIntervalHour"
|
||||
type="number" min="1"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{{ $t('setting.autoUpdateSub') }}</div>
|
||||
<ElSelect v-model="setting.subscriptionAutoUpdateMode" size="small">
|
||||
<ElOption value="none" :label="$t('setting.options.off')" />
|
||||
<ElOption value="auto_update" :label="$t('setting.options.updateSubWhenStart')" />
|
||||
<ElOption value="auto_update_at_intervals" :label="$t('setting.options.updateSubAtIntervals')" />
|
||||
</ElSelect>
|
||||
|
||||
<ElInput
|
||||
v-if="setting.subscriptionAutoUpdateMode === 'auto_update_at_intervals'"
|
||||
ref="autoUpdateSubInput"
|
||||
v-model="setting.subscriptionAutoUpdateIntervalHour"
|
||||
type="number" min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{{ $t('setting.preferModeWhenUpdate') }}</div>
|
||||
<ElSelect v-model="setting.proxyModeWhenSubscribe" size="small">
|
||||
<ElOption
|
||||
value="direct"
|
||||
:label="setting.transparent === 'close' || setting.lite
|
||||
? $t('setting.options.direct')
|
||||
: $t('setting.options.dependTransparentMode')
|
||||
"
|
||||
/>
|
||||
<ElOption value="proxy" :label="$t('setting.options.global')" />
|
||||
<ElOption value="pac" :label="$t('setting.options.pac')" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<ElButton @click="updateSetting">
|
||||
{{ $t('operations.saveApply') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
@apply flex space-x-2 my-1 justify-items-baseline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.el-checkbox-button__inner {
|
||||
padding: 4px 12px;
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-left-color: rgb(229, 231, 235);
|
||||
}
|
||||
</style>
|
55
ngui/pages/signup.vue
Normal file
55
ngui/pages/signup.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
if (user.value.exist) navigateTo('/login')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const username = $ref('')
|
||||
const password = $ref('')
|
||||
|
||||
async function signup() {
|
||||
const { data } = await useV2Fetch<any>('account').post({ username, password }).json()
|
||||
|
||||
if (data.value.code !== 'SUCCESS') {
|
||||
ElMessage.warning({ message: data.value.message, duration: 5000 })
|
||||
} else {
|
||||
user.value.exist = true
|
||||
user.value.token = data.value.data.token
|
||||
ElMessage.success(t('common.success'))
|
||||
navigateTo('/')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto w-96">
|
||||
<h1 class="text-2xl mb-6">{{ t('register.title') }}</h1>
|
||||
|
||||
<ElForm label-width="auto">
|
||||
<ElFormItem :label="t('login.username')">
|
||||
<ElInput v-model="username" autofocus />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="t('login.password')">
|
||||
<ElInput v-model="password" type="password" max-length="36" show-password />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" class="flex mx-auto" @click="signup">{{ t("operations.create") }}</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="mt-4 bg-gray-200 p-4 rounded-sm" />
|
||||
|
||||
<ElAlert type="info" show-icon :closable="false">
|
||||
<p>{{ t("register.messages.0") }}</p>
|
||||
<p>{{ t("register.messages.1") }}</p>
|
||||
<p>{{ t("register.messages.2") }}</p>
|
||||
</ElAlert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.va-input-wrapper--labeled .va-input-wrapper__label {
|
||||
height: 14px;
|
||||
}
|
||||
</style>
|
7325
ngui/pnpm-lock.yaml
Normal file
7325
ngui/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
4
ngui/tsconfig.json
Normal file
4
ngui/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
20
ngui/uno.config.ts
Normal file
20
ngui/uno.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {
|
||||
defineConfig,
|
||||
presetAttributify, presetIcons, presetTagify,
|
||||
presetTypography, presetUno,
|
||||
transformerDirectives, transformerVariantGroup
|
||||
} from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetTypography(),
|
||||
presetAttributify({ strict: true }),
|
||||
presetIcons({ prefix: '' }),
|
||||
presetTagify()
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives(),
|
||||
transformerVariantGroup()
|
||||
]
|
||||
})
|
Loading…
Reference in New Issue
Block a user