Merge 3208c975bb9f7d55e96c326e3f910aafa26b61c2 into f47d8ab97fd48b8ced82eb143bc52126db51a19e

This commit is contained in:
nkdns 2024-12-17 21:21:21 +08:00 committed by GitHub
commit b3761f5701
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 668 additions and 360 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>frp client admin UI</title> <title>frp client admin UI</title>
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script> <script type="module" crossorigin src="./index-Cgx39LXC.js"></script>
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css"> <link rel="stylesheet" crossorigin href="./index-N4E3zN8T.css">
</head> </head>
<body> <body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>frps dashboard</title> <title>frps dashboard</title>
<script type="module" crossorigin src="./index-82-40HIG.js"></script> <script type="module" crossorigin src="./index-yOnBcT7d.js"></script>
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css"> <link rel="stylesheet" crossorigin href="./index-5A9aPAsI.css">
</head> </head>
<body> <body>

View File

@ -10,6 +10,9 @@ declare module 'vue' {
ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default'] ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"element-plus": "^2.5.3", "element-plus": "^2.5.3",
"vue": "^3.4.15", "vue": "^3.4.15",
"vue-i18n": "^10.0.5",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {

View File

@ -3,36 +3,39 @@
<header class="grid-content header-color"> <header class="grid-content header-color">
<div class="header-content"> <div class="header-content">
<div class="brand"> <div class="brand">
<a href="#">frp client</a> <a href="#">{{ t("main.title") }}</a>
</div>
<div class="right-ability">
<div class="lang-switch">
<el-dropdown>
<img src="./assets/lang.svg" alt="lang">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="locale == 'en' ? true : false"
@click="switchLanguage('en')">English</el-dropdown-item>
<el-dropdown-item :disabled="locale == 'zh' ? true : false"
@click="switchLanguage('zh')">简体中文</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
<div class="dark-switch"> <div class="dark-switch">
<el-switch <el-switch v-model="darkmodeSwitch" inline-prompt :active-text="t('main.dark.Dark')"
v-model="darkmodeSwitch" :inactive-text="t('main.dark.Light')" @change="toggleDark" style="
inline-prompt
active-text="Dark"
inactive-text="Light"
@change="toggleDark"
style="
--el-switch-on-color: #444452; --el-switch-on-color: #444452;
--el-switch-off-color: #589ef8; --el-switch-off-color: #589ef8;
" " />
/> </div>
</div> </div>
</div> </div>
</header> </header>
<section> <section>
<el-row> <el-row>
<el-col id="side-nav" :xs="24" :md="4"> <el-col id="side-nav" :xs="24" :md="4">
<el-menu <el-menu default-active="1" mode="vertical" theme="light" router="false" @select="handleSelect">
default-active="1" <el-menu-item index="/">{{ t("main.Overview") }}</el-menu-item>
mode="vertical" <el-menu-item index="/configure">{{ t("main.Configure") }}</el-menu-item>
theme="light" <el-menu-item index="">{{ t("main.Help") }}</el-menu-item>
router="false"
@select="handleSelect"
>
<el-menu-item index="/">Overview</el-menu-item>
<el-menu-item index="/configure">Configure</el-menu-item>
<el-menu-item index="">Help</el-menu-item>
</el-menu> </el-menu>
</el-col> </el-col>
@ -50,11 +53,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useDark, useToggle } from '@vueuse/core' import { useDark, useToggle } from '@vueuse/core'
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const isDark = useDark() const isDark = useDark()
const darkmodeSwitch = ref(isDark) const darkmodeSwitch = ref(isDark)
const toggleDark = useToggle(isDark) const toggleDark = useToggle(isDark)
const switchLanguage = (lang: string) => {
locale.value = lang;
localStorage.setItem('i18n', lang);
}
const handleSelect = (key: string) => { const handleSelect = (key: string) => {
if (key == '') { if (key == '') {
window.open('https://github.com/fatedier/frp') window.open('https://github.com/fatedier/frp')
@ -107,10 +115,20 @@ html.dark .header-color {
text-decoration: none; text-decoration: none;
} }
.dark-switch { .right-ability {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
flex-grow: 1; flex-grow: 1;
padding-right: 40px; padding-right: 40px;
} }
.lang-switch {
width: 30px;
margin-right: 10px;
}
.lang-switch img {
width: 100%;
height: auto;
}
</style> </style>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732765556989" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1552" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M864 64a96 96 0 0 1 96 96v704a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96h704z m0 64H160a32 32 0 0 0-32 32v704a32 32 0 0 0 32 32h704a32 32 0 0 0 32-32V160a32 32 0 0 0-32-32z m-322.4 256c0-31.456 40.64-44.032 58.4-18.08l133.6 195.168V384a32 32 0 0 1 64 0v280.48c0 31.456-40.64 44.032-58.4 18.08l-133.6-195.168v177.088a32 32 0 1 1-64 0z" fill="#ffffff" p-id="1553"></path><path d="M448 352a32 32 0 0 1 0 64H288v80h160a32 32 0 0 1 31.776 28.256L480 528a32 32 0 0 1-32 32H288v72.48h160a32 32 0 1 1 0 64H256a32 32 0 0 1-32-32V384a32 32 0 0 1 32-32z" fill="#ffffff" p-id="1554"></path></svg>

After

Width:  |  Height:  |  Size: 936 B

View File

@ -0,0 +1,25 @@
{
"main": {
"title": "frp client",
"Overview": "Overview",
"Configure": "Configure",
"Help": "Help",
"dark": {
"Dark": "Dark",
"Light": "Light"
}
},
"OverView": {
"name": "name",
"type": "type",
"local_addr": "local address",
"plugin": "plugin",
"remote_addr": "remote address",
"status": "status",
"err": "info"
},
"Configure": {
"Refresh": "Refresh",
"Upload": "Upload"
}
}

View File

@ -0,0 +1,25 @@
{
"main": {
"title": "frp 客户端",
"Overview": "总览",
"Configure": "配置",
"Help": "帮助",
"dark": {
"Dark": "暗",
"Light": "明"
}
},
"OverView": {
"name": "通道名称",
"type": "协议类型",
"local_addr": "本地地址",
"plugin": "插件",
"remote_addr": "远程地址",
"status": "状态",
"err": "信息"
},
"Configure": {
"Refresh": "读取配置",
"Upload": "保存配置"
}
}

View File

@ -1,21 +1,19 @@
<template> <template>
<div> <div>
<el-row id="head"> <el-row id="head">
<el-button type="primary" @click="fetchData">Refresh</el-button> <el-button type="primary" @click="fetchData">{{ t("Configure.Refresh") }}</el-button>
<el-button type="primary" @click="uploadConfig">Upload</el-button> <el-button type="primary" @click="uploadConfig">{{ t("Configure.Upload") }}</el-button>
</el-row> </el-row>
<el-input <el-input type="textarea" autosize v-model="textarea"
type="textarea" placeholder="frpc configrue file, can not be empty..."></el-input>
autosize
v-model="textarea"
placeholder="frpc configrue file, can not be empty..."
></el-input>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
let textarea = ref('') let textarea = ref('')

View File

@ -3,47 +3,14 @@
<el-row> <el-row>
<el-col :md="24"> <el-col :md="24">
<div> <div>
<el-table <el-table :data="status" stripe style="width: 100%" :default-sort="{ prop: 'type', order: 'ascending' }">
:data="status" <el-table-column prop="name" :label="t('OverView.name')" sortable></el-table-column>
stripe <el-table-column prop="type" :label="t('OverView.type')" width="150" sortable></el-table-column>
style="width: 100%" <el-table-column prop="local_addr" :label="t('OverView.local_addr')" width="200" sortable></el-table-column>
:default-sort="{ prop: 'type', order: 'ascending' }" <el-table-column prop="plugin" :label="t('OverView.plugin')" width="200" sortable></el-table-column>
> <el-table-column prop="remote_addr" :label="t('OverView.remote_addr')" sortable></el-table-column>
<el-table-column <el-table-column prop="status" :label="t('OverView.status')" width="150" sortable></el-table-column>
prop="name" <el-table-column prop="err" :label="t('OverView.err')"></el-table-column>
label="name"
sortable
></el-table-column>
<el-table-column
prop="type"
label="type"
width="150"
sortable
></el-table-column>
<el-table-column
prop="local_addr"
label="local address"
width="200"
sortable
></el-table-column>
<el-table-column
prop="plugin"
label="plugin"
width="200"
sortable
></el-table-column>
<el-table-column
prop="remote_addr"
label="remote address"
sortable
></el-table-column>
<el-table-column
prop="status"
label="status"
width="150"
sortable
></el-table-column>
<el-table-column prop="err" label="info"></el-table-column>
</el-table> </el-table>
</div> </div>
</el-col> </el-col>
@ -54,6 +21,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
let status = ref<any[]>([]) let status = ref<any[]>([])

View File

@ -1,13 +1,24 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createI18n } from 'vue-i18n';
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css' import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import './assets/dark.css' import './assets/dark.css'
import en from './assets/locales/en.json';
import zh from './assets/locales/zh.json';
const storedLocale = localStorage.getItem('i18n') || 'en';
const i18n = createI18n({
locale: storedLocale, // 默认语言
messages: {
en,
zh,
},
});
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
app.use(i18n);
app.mount('#app') app.mount('#app')

View File

@ -205,6 +205,27 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917"
integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==
"@intlify/core-base@10.0.5":
version "10.0.5"
resolved "https://registry.npmmirror.com/@intlify/core-base/-/core-base-10.0.5.tgz#c4d992381f8c3a50c79faf67be3404b399c3be28"
integrity sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==
dependencies:
"@intlify/message-compiler" "10.0.5"
"@intlify/shared" "10.0.5"
"@intlify/message-compiler@10.0.5":
version "10.0.5"
resolved "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-10.0.5.tgz#4eeace9f4560020d5e5d77f32bed7755e71d8efd"
integrity sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==
dependencies:
"@intlify/shared" "10.0.5"
source-map-js "^1.0.2"
"@intlify/shared@10.0.5":
version "10.0.5"
resolved "https://registry.npmmirror.com/@intlify/shared/-/shared-10.0.5.tgz#1b46ca8b541f03508fe28da8f34e4bb85506d6bc"
integrity sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==
"@jridgewell/sourcemap-codec@^1.4.15": "@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15" version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
@ -2500,6 +2521,15 @@ vue-eslint-parser@^9.3.1, vue-eslint-parser@^9.4.2:
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.6" semver "^7.3.6"
vue-i18n@^10.0.5:
version "10.0.5"
resolved "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-10.0.5.tgz#fdf4e6c7b669e80cfa3a12ed9625e2b46671cdf0"
integrity sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==
dependencies:
"@intlify/core-base" "10.0.5"
"@intlify/shared" "10.0.5"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.2.5: vue-router@^4.2.5:
version "4.2.5" version "4.2.5"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"

View File

@ -11,6 +11,9 @@ declare module 'vue' {
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']

View File

@ -16,6 +16,7 @@
"element-plus": "^2.5.3", "element-plus": "^2.5.3",
"humanize-plus": "^1.8.2", "humanize-plus": "^1.8.2",
"vue": "^3.4.15", "vue": "^3.4.15",
"vue-i18n": "^10.0.5",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {

View File

@ -3,37 +3,40 @@
<header class="grid-content header-color"> <header class="grid-content header-color">
<div class="header-content"> <div class="header-content">
<div class="brand"> <div class="brand">
<a href="#">frp</a> <a href="#">{{ t("main.title") }}</a>
</div>
<div class="right-ability">
<div class="lang-switch">
<el-dropdown>
<img src="./assets/lang.svg" alt="lang">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="locale == 'en' ? true : false"
@click="switchLanguage('en')">English</el-dropdown-item>
<el-dropdown-item :disabled="locale == 'zh' ? true : false"
@click="switchLanguage('zh')">简体中文</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
<div class="dark-switch"> <div class="dark-switch">
<el-switch <el-switch v-model="darkmodeSwitch" inline-prompt :active-text="t('main.dark.Dark')"
v-model="darkmodeSwitch" :inactive-text="t('main.dark.Light')" @change="toggleDark" style="
inline-prompt
active-text="Dark"
inactive-text="Light"
@change="toggleDark"
style="
--el-switch-on-color: #444452; --el-switch-on-color: #444452;
--el-switch-off-color: #589ef8; --el-switch-off-color: #589ef8;
" " />
/> </div>
</div> </div>
</div> </div>
</header> </header>
<section> <section>
<el-row> <el-row>
<el-col id="side-nav" :xs="24" :md="4"> <el-col id="side-nav" :xs="24" :md="4">
<el-menu <el-menu default-active="/" mode="vertical" theme="light" router="false" @select="handleSelect">
default-active="/" <el-menu-item index="/">{{ t("main.Overview") }}</el-menu-item>
mode="vertical"
theme="light"
router="false"
@select="handleSelect"
>
<el-menu-item index="/">Overview</el-menu-item>
<el-sub-menu index="/proxies"> <el-sub-menu index="/proxies">
<template #title> <template #title>
<span>Proxies</span> <span>{{ t("main.Proxies.title") }}</span>
</template> </template>
<el-menu-item index="/proxies/tcp">TCP</el-menu-item> <el-menu-item index="/proxies/tcp">TCP</el-menu-item>
<el-menu-item index="/proxies/udp">UDP</el-menu-item> <el-menu-item index="/proxies/udp">UDP</el-menu-item>
@ -43,7 +46,7 @@
<el-menu-item index="/proxies/stcp">STCP</el-menu-item> <el-menu-item index="/proxies/stcp">STCP</el-menu-item>
<el-menu-item index="/proxies/sudp">SUDP</el-menu-item> <el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-menu-item index="">Help</el-menu-item> <el-menu-item index="">{{ t("main.Help") }}</el-menu-item>
</el-menu> </el-menu>
</el-col> </el-col>
@ -61,11 +64,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useDark, useToggle } from '@vueuse/core' import { useDark, useToggle } from '@vueuse/core'
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const isDark = useDark() const isDark = useDark()
const darkmodeSwitch = ref(isDark) const darkmodeSwitch = ref(isDark)
const toggleDark = useToggle(isDark) const toggleDark = useToggle(isDark)
const switchLanguage = (lang: string) => {
locale.value = lang;
localStorage.setItem('i18n', lang);
}
const handleSelect = (key: string) => { const handleSelect = (key: string) => {
if (key == '') { if (key == '') {
window.open('https://github.com/fatedier/frp') window.open('https://github.com/fatedier/frp')
@ -118,10 +126,20 @@ html.dark .header-color {
text-decoration: none; text-decoration: none;
} }
.dark-switch { .right-ability {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
flex-grow: 1; flex-grow: 1;
padding-right: 40px; padding-right: 40px;
} }
.lang-switch {
width: 30px;
margin-right: 10px;
}
.lang-switch img {
width: 100%;
height: auto;
}
</style> </style>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732765556989" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1552" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M864 64a96 96 0 0 1 96 96v704a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96h704z m0 64H160a32 32 0 0 0-32 32v704a32 32 0 0 0 32 32h704a32 32 0 0 0 32-32V160a32 32 0 0 0-32-32z m-322.4 256c0-31.456 40.64-44.032 58.4-18.08l133.6 195.168V384a32 32 0 0 1 64 0v280.48c0 31.456-40.64 44.032-58.4 18.08l-133.6-195.168v177.088a32 32 0 1 1-64 0z" fill="#ffffff" p-id="1553"></path><path d="M448 352a32 32 0 0 1 0 64H288v80h160a32 32 0 0 1 31.776 28.256L480 528a32 32 0 0 1-32 32H288v72.48h160a32 32 0 1 1 0 64H256a32 32 0 0 1-32-32V384a32 32 0 0 1 32-32z" fill="#ffffff" p-id="1554"></path></svg>

After

Width:  |  Height:  |  Size: 936 B

View File

@ -0,0 +1,79 @@
{
"main": {
"title": "frp",
"Overview": "Overview",
"Proxies": {
"title": "Proxies"
},
"Help": "Help",
"dark": {
"Dark": "Dark",
"Light": "Light"
}
},
"OverView": {
"Version": "Version",
"BindPort": "BindPort",
"KPC_Port": "KCP Bind Port",
"QUIC_Port": "QUIC Bind Port",
"HTTP_Port": "HTTP Port",
"HTTPS_Port": "HTTPS Port",
"TCPMux": "TCPMux HTTPConnect Port",
"Subdomain": "Subdomain Host",
"MaxPoolCount": "Max PoolCount",
"MaxPortsPerClient": "Max Ports Per Client",
"AllowPorts": "Allow Ports",
"TLSForce": "TLS Force",
"HeartBeatTimeout": "HeartBeat Timeout",
"ClientCounts": "Client Counts",
"curConns": "Current Connections",
"ProxyCounts": "Proxy Counts",
"Chart": {
"Traffic": {
"title": "Network Traffic",
"subTitle": "today",
"TrafficIn": "Traffic In",
"TrafficOut": "Traffic Out"
},
"Proxies": {
"title": "Proxies",
"subTitle": "now"
}
}
},
"ProxiesView": {
"Expand": {
"Name": "Name",
"Type": "Type",
"Encryption": "Encryption",
"Compression": "Compression",
"LastStart": "Last Start",
"LastClose": "Last Close",
"Domains": "Domains",
"SubDomain": "SubDomain",
"locations": "locations",
"HostRewrite": "HostRewrite",
"Multiplexer": "Multiplexer",
"RouteByHTTPUser": "RouteByHTTPUser",
"Addr": "Addr",
"Annotations": "Annotations"
},
"ClearOffLine": "ClearOfflineProxies",
"Refresh": "Refresh",
"ClearOffLineInfo": "Are you sure to clear all data of offline proxies?",
"name": "Name",
"Port": "Port",
"Connections": "Connections",
"Traffic_In": "Traffic In",
"Traffic_Out": "Traffic Out",
"ClientVersion": "ClientVersion",
"Status": {
"title": "Status",
"Successinfo": "online"
},
"Operations": {
"title": "Operations",
"Traffic": "Traffic"
}
}
}

View File

@ -0,0 +1,88 @@
{
"main": {
"title": "frp 服务端",
"Overview": "总览",
"Proxies": {
"title": "代理"
},
"Help": "帮助",
"dark": {
"Dark": "暗",
"Light": "明"
}
},
"OverView": {
"Expand": {
"Name": "通道名称",
"Type": "协议类型",
"Encryption": "加密",
"Compression": "压缩",
"LastStart": "最后一次启用时间",
"LastClose": "最后一次关闭时间",
"Domains": "域名",
"SubDomain": "二级域名后缀",
"locations": "URL 路由配置",
"HostRewrite": "替换 Host Header",
"Multiplexer": "复用器类型",
"RouteByHTTPUser": "根据 HTTP Basic Auth user 路由",
"Addr": "代理端口",
"Annotations": "注释"
},
"Version": "版本",
"BindPort": "frp 绑定端口",
"KPC_Port": "KCP 绑定端口",
"QUIC_Port": "QUIC 绑定端口",
"HTTP_Port": "HTTP 代理监听端口",
"HTTPS_Port": "HTTPS 代理监听端口",
"TCPMux": "TCPMux HTTPConnect 代理监听的端口",
"Subdomain": "二级域名后缀",
"MaxPoolCount": "最大连接池数量",
"MaxPortsPerClient": "单客户端最大代理数",
"AllowPorts": "允许代理的服务端端口",
"TLSForce": "TLS协议版本",
"HeartBeatTimeout": "心跳超时时间",
"ClientCounts": "客户端数量",
"curConns": "当前连接数",
"ProxyCounts": "代理数量",
"Chart": {
"Traffic": {
"title": "网络流量",
"subTitle": "今日",
"TrafficIn": "进站流量",
"TrafficOut": "出站流量"
},
"Proxies": {
"title": "代理占比",
"subTitle": "当前"
}
}
},
"ProxiesView": {
"ClearOffLine": "清理离线链接",
"Refresh": "刷新",
"ClearOffLineInfo": "确定清理所有离线数据?",
"name": "代理名称",
"Port": "代理端口",
"Connections": "连接数",
"Traffic_In": "入站流量",
"Traffic_Out": "出站流量",
"ClientVersion": "frp客户端版本",
"Status": {
"title": "状态",
"Successinfo": "在线"
},
"Operations": {
"title": "操作",
"Traffic": "流量"
}
},
"ProxiesViews": {
"name": "通道名称",
"type": "协议类型",
"local_addr": "本地地址",
"plugin": "插件",
"remote_addr": "远程地址",
"status": "状态",
"err": "信息"
}
}

View File

@ -1,85 +1,57 @@
<template> <template>
<div> <div>
<el-page-header <el-page-header :icon="null" style="width: 100%; margin-left: 30px; margin-bottom: 20px">
:icon="null"
style="width: 100%; margin-left: 30px; margin-bottom: 20px"
>
<template #title> <template #title>
<span>{{ proxyType }}</span> <span>{{ proxyType }}</span>
</template> </template>
<template #content> </template> <template #content> </template>
<template #extra> <template #extra>
<div class="flex items-center" style="margin-right: 30px"> <div class="flex items-center" style="margin-right: 30px">
<el-popconfirm <el-popconfirm :title="t('ProxiesView.ClearOffLineInfo')" @confirm="clearOfflineProxies">
title="Are you sure to clear all data of offline proxies?"
@confirm="clearOfflineProxies"
>
<template #reference> <template #reference>
<el-button>ClearOfflineProxies</el-button> <el-button>{{ t("ProxiesView.ClearOffLine") }}</el-button>
</template> </template>
</el-popconfirm> </el-popconfirm>
<el-button @click="$emit('refresh')">Refresh</el-button> <el-button @click="$emit('refresh')">{{ t("ProxiesView.Refresh") }}</el-button>
</div> </div>
</template> </template>
</el-page-header> </el-page-header>
<el-table <el-table :data="proxies" :default-sort="{ prop: 'name', order: 'ascending' }" style="width: 100%">
:data="proxies"
:default-sort="{ prop: 'name', order: 'ascending' }"
style="width: 100%"
>
<el-table-column type="expand"> <el-table-column type="expand">
<template #default="props"> <template #default="props">
<ProxyViewExpand :row="props.row" :proxyType="proxyType" /> <ProxyViewExpand :row="props.row" :proxyType="proxyType" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="Name" prop="name" sortable> </el-table-column> <el-table-column :label="t('ProxiesView.name')" prop="name" sortable> </el-table-column>
<el-table-column label="Port" prop="port" sortable> </el-table-column> <el-table-column :label="t('ProxiesView.Port')" prop="port" sortable> </el-table-column>
<el-table-column label="Connections" prop="conns" sortable> <el-table-column :label="t('ProxiesView.Connections')" prop="conns" sortable>
</el-table-column> </el-table-column>
<el-table-column <el-table-column :label="t('ProxiesView.Traffic_In')" prop="trafficIn" :formatter="formatTrafficIn" sortable>
label="Traffic In"
prop="trafficIn"
:formatter="formatTrafficIn"
sortable
>
</el-table-column> </el-table-column>
<el-table-column <el-table-column :label="t('ProxiesView.Traffic_Out')" prop="trafficOut" :formatter="formatTrafficOut" sortable>
label="Traffic Out"
prop="trafficOut"
:formatter="formatTrafficOut"
sortable
>
</el-table-column> </el-table-column>
<el-table-column label="ClientVersion" prop="clientVersion" sortable> <el-table-column :label="t('ProxiesView.ClientVersion')" prop="clientVersion" sortable>
</el-table-column> </el-table-column>
<el-table-column label="Status" prop="status" sortable> <el-table-column :label="t('ProxiesView.Status.title')" prop="status" sortable>
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status === 'online'" type="success">{{ <el-tag v-if="scope.row.status === 'online'" type="success">{{
scope.row.status t("ProxiesView.Status.Successinfo")
}}</el-tag> }}</el-tag>
<el-tag v-else type="danger">{{ scope.row.status }}</el-tag> <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="Operations"> <el-table-column :label="t('ProxiesView.Operations.title')">
<template #default="scope"> <template #default="scope">
<el-button <el-button type="primary" :name="scope.row.name" style="margin-bottom: 10px"
type="primary" @click="dialogVisibleName = scope.row.name; dialogVisible = true">{{ t("ProxiesView.Operations.Traffic") }}
:name="scope.row.name"
style="margin-bottom: 10px"
@click="dialogVisibleName = scope.row.name; dialogVisible = true"
>Traffic
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
<el-dialog <el-dialog v-model="dialogVisible" destroy-on-close="true" :title="dialogVisibleName" width="700px">
v-model="dialogVisible"
destroy-on-close="true"
:title="dialogVisibleName"
width="700px">
<Traffic :proxyName="dialogVisibleName" /> <Traffic :proxyName="dialogVisibleName" />
</el-dialog> </el-dialog>
</template> </template>
@ -91,6 +63,8 @@ import type { BaseProxy } from '../utils/proxy.js'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import ProxyViewExpand from './ProxyViewExpand.vue' import ProxyViewExpand from './ProxyViewExpand.vue'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
defineProps<{ defineProps<{
proxies: BaseProxy[] proxies: BaseProxy[]

View File

@ -1,59 +1,54 @@
<template> <template>
<el-form <el-form label-position="left" label-width="auto" inline class="proxy-table-expand">
label-position="left" <el-form-item :label="t('OverView.Expand.Name')">
label-width="auto"
inline
class="proxy-table-expand"
>
<el-form-item label="Name">
<span>{{ row.name }}</span> <span>{{ row.name }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Type"> <el-form-item :label="t('OverView.Expand.Type')">
<span>{{ row.type }}</span> <span>{{ row.type }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Encryption"> <el-form-item :label="t('OverView.Expand.Encryption')">
<span>{{ row.encryption }}</span> <span>{{ row.encryption }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Compression"> <el-form-item :label="t('OverView.Expand.Compression')">
<span>{{ row.compression }}</span> <span>{{ row.compression }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Last Start"> <el-form-item :label="t('OverView.Expand.LastStart')">
<span>{{ row.lastStartTime }}</span> <span>{{ row.lastStartTime }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Last Close"> <el-form-item :label="t('OverView.Expand.LastClose')">
<span>{{ row.lastCloseTime }}</span> <span>{{ row.lastCloseTime }}</span>
</el-form-item> </el-form-item>
<div v-if="proxyType === 'http' || proxyType === 'https'"> <div v-if="proxyType === 'http' || proxyType === 'https'">
<el-form-item label="Domains"> <el-form-item :label="t('OverView.Expand.Domains')">
<span>{{ row.customDomains }}</span> <span>{{ row.customDomains }}</span>
</el-form-item> </el-form-item>
<el-form-item label="SubDomain"> <el-form-item :label="t('OverView.Expand.SubDomain')">
<span>{{ row.subdomain }}</span> <span>{{ row.subdomain }}</span>
</el-form-item> </el-form-item>
<el-form-item label="locations"> <el-form-item :label="t('OverView.Expand.locations')">
<span>{{ row.locations }}</span> <span>{{ row.locations }}</span>
</el-form-item> </el-form-item>
<el-form-item label="HostRewrite"> <el-form-item :label="t('OverView.Expand.HostRewrite')">
<span>{{ row.hostHeaderRewrite }}</span> <span>{{ row.hostHeaderRewrite }}</span>
</el-form-item> </el-form-item>
</div> </div>
<div v-else-if="proxyType === 'tcpmux'"> <div v-else-if="proxyType === 'tcpmux'">
<el-form-item label="Multiplexer"> <el-form-item :label="t('OverView.Expand.Multiplexer')">
<span>{{ row.multiplexer }}</span> <span>{{ row.multiplexer }}</span>
</el-form-item> </el-form-item>
<el-form-item label="RouteByHTTPUser"> <el-form-item :label="t('OverView.Expand.RouteByHTTPUser')">
<span>{{ row.routeByHTTPUser }}</span> <span>{{ row.routeByHTTPUser }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Domains"> <el-form-item :label="t('OverView.Expand.Domains')">
<span>{{ row.customDomains }}</span> <span>{{ row.customDomains }}</span>
</el-form-item> </el-form-item>
<el-form-item label="SubDomain"> <el-form-item :label="t('OverView.Expand.SubDomain')">
<span>{{ row.subdomain }}</span> <span>{{ row.subdomain }}</span>
</el-form-item> </el-form-item>
</div> </div>
<div v-else> <div v-else>
<el-form-item label="Addr"> <el-form-item :label="t('OverView.Expand.Addr')">
<span>{{ row.addr }}</span> <span>{{ row.addr }}</span>
</el-form-item> </el-form-item>
</div> </div>
@ -61,7 +56,7 @@
<div v-if="row.annotations && row.annotations.size > 0"> <div v-if="row.annotations && row.annotations.size > 0">
<el-divider /> <el-divider />
<el-text class="title-text" size="large">Annotations</el-text> <el-text class="title-text" size="large">{{ t("OverView.Expand.Annotations") }}</el-text>
<ul> <ul>
<li v-for="item in annotationsArray()"> <li v-for="item in annotationsArray()">
<span class="annotation-key">{{ item.key }}</span> <span class="annotation-key">{{ item.key }}</span>
@ -72,6 +67,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
row: any row: any

View File

@ -3,73 +3,60 @@
<el-row> <el-row>
<el-col :md="12"> <el-col :md="12">
<div class="source"> <div class="source">
<el-form <el-form label-position="left" label-width="220px" class="server_info">
label-position="left" <el-form-item :label="t('OverView.Version')">
label-width="220px"
class="server_info"
>
<el-form-item label="Version">
<span>{{ data.version }}</span> <span>{{ data.version }}</span>
</el-form-item> </el-form-item>
<el-form-item label="BindPort"> <el-form-item :label="t('OverView.BindPort')">
<span>{{ data.bindPort }}</span> <span>{{ data.bindPort }}</span>
</el-form-item> </el-form-item>
<el-form-item label="KCP Bind Port" v-if="data.kcpBindPort != 0"> <el-form-item :label="t('OverView.KPC_Port')" v-if="data.kcpBindPort != 0">
<span>{{ data.kcpBindPort }}</span> <span>{{ data.kcpBindPort }}</span>
</el-form-item> </el-form-item>
<el-form-item label="QUIC Bind Port" v-if="data.quicBindPort != 0"> <el-form-item :label="t('OverView.QUIC_Port')" v-if="data.quicBindPort != 0">
<span>{{ data.quicBindPort }}</span> <span>{{ data.quicBindPort }}</span>
</el-form-item> </el-form-item>
<el-form-item label="HTTP Port" v-if="data.vhostHTTPPort != 0"> <el-form-item :label="t('OverView.HTTP_Port')" v-if="data.vhostHTTPPort != 0">
<span>{{ data.vhostHTTPPort }}</span> <span>{{ data.vhostHTTPPort }}</span>
</el-form-item> </el-form-item>
<el-form-item label="HTTPS Port" v-if="data.vhostHTTPSPort != 0"> <el-form-item :label="t('OverView.HTTPS_Port')" v-if="data.vhostHTTPSPort != 0">
<span>{{ data.vhostHTTPSPort }}</span> <span>{{ data.vhostHTTPSPort }}</span>
</el-form-item> </el-form-item>
<el-form-item <el-form-item :label="t('OverView.TCPMux')" v-if="data.tcpmuxHTTPConnectPort != 0">
label="TCPMux HTTPConnect Port"
v-if="data.tcpmuxHTTPConnectPort != 0"
>
<span>{{ data.tcpmuxHTTPConnectPort }}</span> <span>{{ data.tcpmuxHTTPConnectPort }}</span>
</el-form-item> </el-form-item>
<el-form-item <el-form-item :label="t('OverView.Subdomain')" v-if="data.subdomainHost != ''">
label="Subdomain Host"
v-if="data.subdomainHost != ''"
>
<LongSpan :content="data.subdomainHost" :length="30"></LongSpan> <LongSpan :content="data.subdomainHost" :length="30"></LongSpan>
</el-form-item> </el-form-item>
<el-form-item label="Max PoolCount"> <el-form-item :label="t('OverView.MaxPoolCount')">
<span>{{ data.maxPoolCount }}</span> <span>{{ data.maxPoolCount }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Max Ports Per Client"> <el-form-item :label="t('OverView.MaxPortsPerClient')">
<span>{{ data.maxPortsPerClient }}</span> <span>{{ data.maxPortsPerClient }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Allow Ports" v-if="data.allowPortsStr != ''"> <el-form-item :label="t('OverView.AllowPorts')" v-if="data.allowPortsStr != ''">
<LongSpan :content="data.allowPortsStr" :length="30"></LongSpan> <LongSpan :content="data.allowPortsStr" :length="30"></LongSpan>
</el-form-item> </el-form-item>
<el-form-item label="TLS Force" v-if="data.tlsForce === true"> <el-form-item :label="t('OverView.TLSForce')" v-if="data.tlsForce === true">
<span>{{ data.tlsForce }}</span> <span>{{ data.tlsForce }}</span>
</el-form-item> </el-form-item>
<el-form-item label="HeartBeat Timeout"> <el-form-item :label="t('OverView.HeartBeatTimeout')">
<span>{{ data.heartbeatTimeout }}</span> <span>{{ data.heartbeatTimeout }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Client Counts"> <el-form-item :label="t('OverView.ClientCounts')">
<span>{{ data.clientCounts }}</span> <span>{{ data.clientCounts }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Current Connections"> <el-form-item :label="t('OverView.curConns')">
<span>{{ data.curConns }}</span> <span>{{ data.curConns }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Proxy Counts"> <el-form-item :label="t('OverView.ProxyCounts')">
<span>{{ data.proxyCounts }}</span> <span>{{ data.proxyCounts }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</el-col> </el-col>
<el-col :md="12"> <el-col :md="12">
<div <div id="traffic" style="width: 400px; height: 250px; margin-bottom: 30px"></div>
id="traffic"
style="width: 400px; height: 250px; margin-bottom: 30px"
></div>
<div id="proxies" style="width: 400px; height: 250px"></div> <div id="proxies" style="width: 400px; height: 250px"></div>
</el-col> </el-col>
</el-row> </el-row>
@ -81,6 +68,8 @@ import { ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { DrawTrafficChart, DrawProxyChart } from '../utils/chart' import { DrawTrafficChart, DrawProxyChart } from '../utils/chart'
import LongSpan from './LongSpan.vue' import LongSpan from './LongSpan.vue'
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
let data = ref({ let data = ref({
version: '', version: '',

View File

@ -1,4 +1,5 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createI18n } from 'vue-i18n';
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css' import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue' import App from './App.vue'
@ -6,9 +7,19 @@ import router from './router'
import './assets/custom.css' import './assets/custom.css'
import './assets/dark.css' import './assets/dark.css'
import en from './assets/locales/en.json';
import zh from './assets/locales/zh.json';
const storedLocale = localStorage.getItem('i18n') || 'en';
const i18n = createI18n({
locale: storedLocale, // 默认语言
messages: {
en,
zh,
},
});
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
app.use(i18n);
app.mount('#app') app.mount('#app')

View File

@ -205,6 +205,27 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917"
integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==
"@intlify/core-base@10.0.5":
version "10.0.5"
resolved "https://registry.npmmirror.com/@intlify/core-base/-/core-base-10.0.5.tgz#c4d992381f8c3a50c79faf67be3404b399c3be28"
integrity sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==
dependencies:
"@intlify/message-compiler" "10.0.5"
"@intlify/shared" "10.0.5"
"@intlify/message-compiler@10.0.5":
version "10.0.5"
resolved "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-10.0.5.tgz#4eeace9f4560020d5e5d77f32bed7755e71d8efd"
integrity sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==
dependencies:
"@intlify/shared" "10.0.5"
source-map-js "^1.0.2"
"@intlify/shared@10.0.5":
version "10.0.5"
resolved "https://registry.npmmirror.com/@intlify/shared/-/shared-10.0.5.tgz#1b46ca8b541f03508fe28da8f34e4bb85506d6bc"
integrity sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==
"@jridgewell/sourcemap-codec@^1.4.15": "@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15" version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
@ -2523,6 +2544,15 @@ vue-eslint-parser@^9.3.1, vue-eslint-parser@^9.4.2:
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.6" semver "^7.3.6"
vue-i18n@^10.0.5:
version "10.0.5"
resolved "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-10.0.5.tgz#fdf4e6c7b669e80cfa3a12ed9625e2b46671cdf0"
integrity sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==
dependencies:
"@intlify/core-base" "10.0.5"
"@intlify/shared" "10.0.5"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.2.5: vue-router@^4.2.5:
version "4.2.5" version "4.2.5"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"