refactor: web gui by nuxt (#787)

This commit is contained in:
Percy Ma 2023-06-03 13:01:42 +08:00 committed by GitHub
parent 5e6a72de43
commit a91120826a
35 changed files with 8727 additions and 0 deletions

9
ngui/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
.eslintcache
dist

1
ngui/.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

15
ngui/app.vue Normal file
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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 }
}
}
})

View File

@ -0,0 +1,12 @@
export const proxies
= useLocalStorage<{
currentOutbound: string
outbounds: any[]
subs: any[]
servers: any[]
}>('proxies', {
currentOutbound: 'proxy',
outbounds: [],
subs: [],
servers: []
})

View 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
View File

@ -0,0 +1,5 @@
export const user = useLocalStorage('user', {
token: '',
firstCheck: true,
exist: false
})

6
ngui/layouts/default.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<Header />
<div class="mx-6">
<slot />
</div>
</template>

294
ngui/locales/zh-hans.yaml Normal file
View 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: 该选项设置规则分流端口所使用的路由模式。默认情况下规则分流端口为20172HTTP协议。
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表示端口2220170: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
View File

@ -0,0 +1,8 @@
export default defineNuxtRouteMiddleware(async() => {
if (!user.value.token) {
if (user.value.exist)
return navigateTo('/login')
else
return navigateTo('/signup')
}
})

View 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 })
}
})

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

4
ngui/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

20
ngui/uno.config.ts Normal file
View 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()
]
})