Merge pull request #26 from foroughi1380/main

Add support for Multi-Language
This commit is contained in:
Hossin Asaadi
2022-11-08 18:10:29 +03:30
committed by GitHub
36 changed files with 714 additions and 199 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ x-ui-*.tar.gz
main
release/
access.log
.cache

79
web/assets/js/langs.js Normal file
View File

@@ -0,0 +1,79 @@
supportLangs = [
{
name : "English",
value : "en-US",
icon : "🇺🇸"
},
{
name : "汉语",
value : "zh-Hans",
icon : "🇨🇳"
},
]
function getLang(){
let lang = getCookie('lang')
if (! lang){
if (window.navigator){
lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)){
setCookie('lang' , lang , 150)
}else{
setCookie('lang' , 'en-US' , 150)
window.location.reload();
}
}else{
setCookie('lang' , 'en-US' , 150)
window.location.reload();
}
}
return lang;
}
function setLang(lang){
if (!isSupportLang(lang)){
lang = 'en-US';
}
setCookie('lang' , lang , 150)
window.location.reload();
}
function isSupportLang(lang){
for (l of supportLangs){
if (l.value === lang){
return true;
}
}
return false;
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -33,13 +33,13 @@ function safeBase64(str) {
function formatSecond(second) {
if (second < 60) {
return second.toFixed(0) + ' ';
return second.toFixed(0) + ' s';
} else if (second < 3600) {
return (second / 60).toFixed(0) + ' 分钟';
return (second / 60).toFixed(0) + ' m';
} else if (second < 3600 * 24) {
return (second / 3600).toFixed(0) + ' 小时';
return (second / 3600).toFixed(0) + ' h';
} else {
return (second / 3600 / 24).toFixed(0) + ' ';
return (second / 3600 / 24).toFixed(0) + ' d';
}
}

View File

@@ -12,7 +12,7 @@ type BaseController struct {
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
if isAjax(c) {
pureJsonMsg(c, false, "登录时效已过,请重新登录")
pureJsonMsg(c, false, I18n(c , "pages.login.loginAgain"))
} else {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
@@ -21,3 +21,13 @@ func (a *BaseController) checkLogin(c *gin.Context) {
c.Next()
}
}
func I18n(c *gin.Context , name string, data ...string) string{
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
message, _ := i18n(name)
return message;
}

View File

@@ -54,7 +54,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil {
jsonMsg(c, "获取", err)
jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbounds, nil)
@@ -64,7 +64,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, "添加", err)
jsonMsg(c, I18n(c , "pages.inbounds.addTo"), err)
return
}
user := session.GetLoginUser(c)
@@ -72,7 +72,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
err = a.inboundService.AddInbound(inbound)
jsonMsg(c, "添加", err)
jsonMsg(c, I18n(c , "pages.inbounds.addTo"), err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -81,11 +81,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, "删除", err)
jsonMsg(c, I18n(c , "delete"), err)
return
}
err = a.inboundService.DelInbound(id)
jsonMsg(c, "删除", err)
jsonMsg(c, I18n(c , "delete"), err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -94,7 +94,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, "修改", err)
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
return
}
inbound := &model.Inbound{
@@ -102,11 +102,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
err = c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, "修改", err)
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
return
}
err = a.inboundService.UpdateInbound(inbound)
jsonMsg(c, "修改", err)
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
if err == nil {
a.xrayService.SetToNeedRestart()
}

View File

@@ -39,22 +39,22 @@ func (a *IndexController) index(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, "xui/")
return
}
html(c, "login.html", "登录", nil)
html(c, "login.html", "pages.login.title", nil)
}
func (a *IndexController) login(c *gin.Context) {
var form LoginForm
err := c.ShouldBind(&form)
if err != nil {
pureJsonMsg(c, false, "数据格式错误")
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.invalidFormData"))
return
}
if form.Username == "" {
pureJsonMsg(c, false, "请输入用户名")
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
return
}
if form.Password == "" {
pureJsonMsg(c, false, "请输入密码")
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password)
@@ -62,7 +62,7 @@ func (a *IndexController) login(c *gin.Context) {
if user == nil {
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, "用户名或密码错误")
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.wrongUsernameOrPassword"))
return
} else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
@@ -71,7 +71,7 @@ func (a *IndexController) login(c *gin.Context) {
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, "登录", err)
jsonMsg(c, I18n(c , "pages.login.toasts.successLogin"), err)
}
func (a *IndexController) logout(c *gin.Context) {

View File

@@ -68,7 +68,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
versions, err := a.serverService.GetXrayVersions()
if err != nil {
jsonMsg(c, "获取版本", err)
jsonMsg(c, I18n(c , "getVersion"), err)
return
}
@@ -81,5 +81,5 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version")
err := a.serverService.UpdateXray(version)
jsonMsg(c, "安装 xray", err)
jsonMsg(c, I18n(c , "install") + " xray", err)
}

View File

@@ -40,7 +40,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting()
if err != nil {
jsonMsg(c, "获取设置", err)
jsonMsg(c, I18n(c , "pages.setting.toasts.getSetting"), err)
return
}
jsonObj(c, allSetting, nil)
@@ -50,27 +50,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting)
if err != nil {
jsonMsg(c, "修改设置", err)
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
return
}
err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, "修改设置", err)
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
}
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, "修改用户", err)
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
return
}
user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, "修改用户", errors.New("原用户名或原密码错误"))
jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.originalUserPassIncorrect")))
return
}
if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, "修改用户", errors.New("新用户名和新密码不能为空"))
jsonMsg(c,I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.userPassMustBeNotEmpty")))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -79,10 +79,10 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword
session.SetLoginUser(c, user)
}
jsonMsg(c, "修改用户", err)
jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), err)
}
func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, "重启面板", err)
jsonMsg(c, I18n(c , "pages.setting.restartPanel"), err)
}

View File

@@ -46,12 +46,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg + "成功"
m.Msg = msg + I18n(c , "success")
}
} else {
m.Success = false
m.Msg = msg + "失败: " + err.Error()
logger.Warning(msg+"失败: ", err)
m.Msg = msg + I18n(c , "fail") + ": " + err.Error()
logger.Warning(msg + I18n(c , "fail") + ": ", err)
}
c.JSON(http.StatusOK, m)
}

View File

@@ -30,13 +30,13 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
}
func (a *XUIController) index(c *gin.Context) {
html(c, "index.html", "系统状态", nil)
html(c, "index.html", "pages.index.title", nil)
}
func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "入站列表", nil)
html(c, "inbounds.html", "pages.inbounds.title", nil)
}
func (a *XUIController) setting(c *gin.Context) {
html(c, "setting.html", "设置", nil)
html(c, "setting.html", "pages.setting.title", nil)
}

View File

@@ -12,6 +12,6 @@
display: none;
}
</style>
<title>{{.title}}</title>
<title>{{ i18n .title}}</title>
</head>
{{end}}

View File

@@ -14,6 +14,7 @@
<script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/langs.js"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;

View File

@@ -1,7 +1,7 @@
{{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:ok-text="promptModal.okText" cancel-text="取消">
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"
:autosize="{minRows: 10, maxRows: 20}"
@@ -15,7 +15,7 @@
title: '',
type: '',
value: '',
okText: '确定',
okText: '{{ i18n "sure"}}',
visible: false,
keyEnter(e) {
if (this.type !== 'textarea') {
@@ -38,7 +38,7 @@
title='',
type='text',
value='',
okText='确定',
okText='{{ i18n "sure"}}',
confirm=() => {},
}) {
this.title = title;

View File

@@ -26,6 +26,12 @@
padding-left: 50px;
}
.selectLang{
display: flex;
text-align: center;
justify-content: center;
}
</style>
<body>
<a-layout id="app" v-cloak>
@@ -33,7 +39,7 @@
<a-layout-content>
<a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1>{{ .title }}</h1>
<h1>{{ i18n "pages.login.title" }}</h1>
</a-col>
</a-row>
<a-row type="flex" justify="center">
@@ -53,6 +59,29 @@
</a-form-item>
<a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
</a-form-item>
<a-form-item>
<a-row justify="center" class="selectLang">
<a-col :span="4"><span>Language : </span></a-col>
<a-col :span="6">
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
>
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-col>
@@ -73,6 +102,10 @@
data: {
loading: false,
user: new User(),
lang : ""
},
created(){
this.lang = getLang();
},
methods: {
async login() {

View File

@@ -1,15 +1,15 @@
{{define "menuItems"}}
<a-menu-item key="{{ .base_path }}xui/">
<a-icon type="dashboard"></a-icon>
<span>系统状态</span>
<span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/inbounds">
<a-icon type="user"></a-icon>
<span>入站列表</span>
<span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/setting">
<a-icon type="setting"></a-icon>
<span>面板设置</span>
<span>{{ i18n "menu.setting"}}</span>
</a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!-- <a-icon type="laptop"></a-icon>-->
@@ -18,7 +18,7 @@
<a-sub-menu>
<template slot="title">
<a-icon type="link"></a-icon>
<span>其他</span>
<span>{{ i18n "menu.link"}}</span>
</template>
<a-menu-item key="https://github.com/vaxilu/x-ui/">
<a-icon type="github"></a-icon>
@@ -27,7 +27,7 @@
</a-sub-menu>
<a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon>
<span>退出登录</span>
<span>{{ i18n "menu.logout"}}</span>
</a-menu-item>
{{end}}

View File

@@ -1,23 +1,23 @@
{{define "inboundInfoStream"}}
<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<p>{{ i18n "transmission" }}: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
<p v-else>host: <a-tag color="orange"></a-tag></p>
<p v-else>{{ i18n "host" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
<p v-else>path: <a-tag color="orange"></a-tag></p>
<p v-else>{{ i18n "path" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
</template>
<template v-if="inbound.isQuic">
<p>quic 加密: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
<p>quic 密码: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
<p>quic 伪装: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
<p>quic {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
<p>quic {{ i18n "password" }}: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
<p>quic {{ i18n "camouflage" }}: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
</template>
<template v-if="inbound.isKcp">
<p>kcp 加密: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
<p>kcp 密码: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
<p>kcp {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
<p>kcp {{ i18n "password" }}: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
</template>
<template v-if="inbound.isGrpc">
@@ -25,26 +25,26 @@
</template>
<template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
<p v-if="inbound.xtls">xtls: <a-tag color="green">开启</a-tag></p>
<p v-if="inbound.tls">tls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
<p v-if="inbound.xtls">xtls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
</template>
<template v-else>
<p>tls: <a-tag color="red">关闭</a-tag></p>
<p>tls: <a-tag color="red">{{ i18n "closure" }}</a-tag></p>
</template>
<p v-if="inbound.tls">
tls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '{{ i18n "none" }}' ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '{{ i18n "none" }}' ]]</a-tag>
</p>
{{end}}
{{define "component/inboundInfoComponent"}}
<div>
<p>协议: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
<p>地址: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
<p>端口: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
<p>{{ i18n "protocol"}}: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
<p>{{ i18n "pages.inbounds.address"}}: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
<p>{{ i18n "pages.inbounds.port"}}: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
<template v-if="dbInbound.isVMess">
<p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
@@ -57,22 +57,22 @@
</template>
<template v-if="dbInbound.isTrojan">
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSS">
<p>加密: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
<p>{{ i18n "encryption"}}: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSocks">
<p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isHTTP">
<p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">

View File

@@ -14,25 +14,25 @@
</a-form-item>
<a-form-item>
<span slot="label">
监听 IP
{{ i18n "monitor" }}
<a-tooltip>
<template slot="title">
默认留空即可
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="inbound.listen"></a-input>
</a-form-item>
<a-form-item label="端口">
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input type="number" v-model.number="inbound.port"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
总流量(GB)
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 表示不限制
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -41,10 +41,10 @@
</a-form-item>
<a-form-item>
<span slot="label">
到期时间
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
留空则永不到期
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>

View File

@@ -1,12 +1,12 @@
{{define "form/dokodemo"}}
<a-form layout="inline">
<a-form-item label="目标地址">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>
<a-form-item label="目标端口">
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
</a-form-item>
<a-form-item label="网络">
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>

View File

@@ -1,9 +1,9 @@
{{define "form/http"}}
<a-form layout="inline">
<a-form-item label="用户名">
<a-form-item label='{{ i18n "username"}}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label="密码">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</a-form>

View File

@@ -1,14 +1,14 @@
{{define "form/shadowsocks"}}
<a-form layout="inline">
<a-form-item label="加密">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 165px;">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="密码">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item>
<a-form-item label="网络">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>

View File

@@ -1,18 +1,19 @@
{{define "form/socks"}}
<a-form layout="inline">
<a-form-item label="密码认证">
<!-- <a-form-item label="密码认证">-->
<a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<template v-if="inbound.settings.auth === 'password'">
<a-form-item label="用户名">
<a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label="密码">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</template>
<a-form-item label="启用 udp">
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.settings.udp"

View File

@@ -1,11 +1,11 @@
{{define "form/trojan"}}
<a-form layout="inline">
<a-form-item label="密码">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.clients[0].flow" style="width: 150px">
<a-select-option value=""></a-select-option>
<a-select-option value="" v-text='{{ i18n "none" }}'></a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>

View File

@@ -39,7 +39,7 @@
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.vlesses[0].flow" style="width: 150px">
<a-select-option value=""></a-select-option>
<a-select-option value="" v-text='{{ i18n "none" }}'></a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>

View File

@@ -38,10 +38,10 @@
<a-form-item label="id">
<a-input v-model.trim="inbound.settings.vmesses[0].id"></a-input>
</a-form-item>
<a-form-item label="额外 ID">
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input type="number" v-model.number="inbound.settings.vmesses[0].alterId"></a-input>
</a-form-item>
<a-form-item label="禁用不安全加密">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
</a-form-item>
</a-form>

View File

@@ -5,7 +5,7 @@
sniffing
<a-tooltip>
<template slot="title">
没有特殊需求保持默认即可
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>

View File

@@ -1,7 +1,7 @@
{{define "form/streamSettings"}}
<!-- select stream network -->
<a-form layout="inline">
<a-form-item label="传输">
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="kcp">kcp</a-select-option>

View File

@@ -13,33 +13,33 @@
<!-- tls settings -->
<a-form v-if="inbound.tls || inbound.xtls"
layout="inline">
<a-form-item label="域名">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item>
<a-form-item label="alpn" placeholder="http/1.1,h2">
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
</a-form-item>
<a-form-item label="证书">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile"
button-style="solid">
<a-radio-button :value="true">certificate file path</a-radio-button>
<a-radio-button :value="false">certificate file content</a-radio-button>
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.tls.certs[0].useFile">
<a-form-item label="公钥文件路径">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
</a-form-item>
<a-form-item label="密钥文件路径">
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
</a-form-item>
</template>
<template v-else>
<a-form-item label="公钥内容">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="2"
v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label="密钥内容">
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="2"
v-model="inbound.stream.tls.certs[0].key"></a-input>
</a-form-item>

View File

@@ -1,8 +1,8 @@
{{define "inboundInfoModal"}}
{{template "component/inboundInfo"}}
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok"
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' @ok="infoModal.ok"
:closable="true" :mask-closable="true"
ok-text="复制链接" cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
ok-text='{{ i18n "pages.inbounds.copyLink"}}' cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
<inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
</a-modal>
<script>
@@ -34,7 +34,7 @@
this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
text: () => this.dbInbound.genLink(),
});
this.clipboard.on('success', () => app.$message.success('复制成功'));
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}'));
});
}
},

View File

@@ -10,7 +10,7 @@
title: '',
visible: false,
confirmLoading: false,
okText: '确定',
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
inbound: new Inbound(),
@@ -19,7 +19,7 @@
ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
},
show({ title='', okText='确定', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) {
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
if (inbound) {

View File

@@ -27,15 +27,15 @@
<a-card hoverable style="margin-bottom: 20px;">
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
总上传 / 下载
{{ i18n "pages.inbounds.totalDownUp" }}
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
总用量
{{ i18n "pages.inbounds.totalUsage" }}
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
入站数量
{{ i18n "pages.inbounds.inboundCount" }}
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
</a-row>
@@ -55,20 +55,22 @@
@change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">操作</a>
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
<a-icon type="qrcode"></a-icon>二维码
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>编辑
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon>重置流量
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
<a-menu-item key="delete">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon>删除
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
@@ -83,10 +85,10 @@
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
</template>
<a-tag v-else color="green">无限制</a-tag>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
</template>
<template slot="settings" slot-scope="text, dbInbound">
<a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
<a-button type="link" @click="showInfo(dbInbound)">{{ i18n "check" }}</a-button>
</template>
<template slot="stream" slot-scope="text, dbInbound, index">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
@@ -94,7 +96,7 @@
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else></template>
<template v-else>{{ i18n "none" }}</template>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
@@ -108,7 +110,7 @@
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</a-tag>
</template>
<a-tag v-else color="green">无限期</a-tag>
<a-tag v-else color="green">{{ i18n "indefinitely" }}</a-tag>
</template>
</a-table>
</a-card>
@@ -121,52 +123,52 @@
<script>
const columns = [{
title: "操作",
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 30,
scopedSlots: { customRender: 'action' },
}, {
title: "启用",
title: '{{ i18n "pages.inbounds.enable" }}',
align: 'center',
width: 40,
scopedSlots: { customRender: 'enable' },
}, {
title: "id",
title: "Id",
align: 'center',
dataIndex: "id",
width: 30,
}, {
title: "备注",
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
width: 100,
dataIndex: "remark",
}, {
title: "协议",
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'center',
width: 60,
scopedSlots: { customRender: 'protocol' },
}, {
title: "端口",
title: '{{ i18n "pages.inbounds.port" }}',
align: 'center',
dataIndex: "port",
width: 60,
}, {
title: "流量↑|↓",
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
align: 'center',
width: 150,
scopedSlots: { customRender: 'traffic' },
}, {
title: "详细信息",
title: '{{ i18n "pages.inbounds.details" }}',
align: 'center',
width: 40,
scopedSlots: { customRender: 'settings' },
}, {
title: "传输配置",
title: '{{ i18n "pages.inbounds.transportConfig" }}',
align: 'center',
width: 60,
scopedSlots: { customRender: 'stream' },
}, {
title: "到期时间",
title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center',
width: 80,
scopedSlots: { customRender: 'expiryTime' },
@@ -234,8 +236,9 @@
},
openAddInbound() {
inModal.show({
title: '添加入站',
okText: '添加',
title: '{{ i18n "pages.inbounds.addInbound"}}',
okText: '{{ i18n "pages.inbounds.addTo"}}',
cancelText: '{{ i18n "close" }}',
confirm: async (inbound, dbInbound) => {
inModal.loading();
await this.addInbound(inbound, dbInbound);
@@ -247,8 +250,9 @@
openEditInbound(dbInbound) {
const inbound = dbInbound.toInbound();
inModal.show({
title: '修改入站',
okText: '修改',
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
okText: '{{ i18n "pages.inbounds.revise"}}',
cancelText: '{{ i18n "close" }}',
inbound: inbound,
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
@@ -297,10 +301,10 @@
},
resetTraffic(dbInbound) {
this.$confirm({
title: '重置流量',
content: '确定要重置流量吗?',
okText: '重置',
cancelText: '取消',
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
const inbound = dbInbound.toInbound();
dbInbound.up = 0;
@@ -311,16 +315,16 @@
},
delInbound(dbInbound) {
this.$confirm({
title: '删除入站',
content: '确定要删除入站吗?',
okText: '删除',
cancelText: '取消',
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
});
},
showQrcode(dbInbound) {
const link = dbInbound.genLink();
qrModal.show('二维码', link);
qrModal.show('{{ i18n "qrCode"}}', link);
},
showInfo(dbInbound) {
infoModal.show(dbInbound);

View File

@@ -35,7 +35,7 @@
:stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress>
<div>
内存: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
</div>
</a-col>
</a-row>
@@ -55,7 +55,7 @@
:stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress>
<div>
硬盘: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
@@ -68,7 +68,7 @@
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable>
xray 状态:
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error">
<template slot="title">
@@ -77,16 +77,16 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" @click="openSelectV2rayVersion">切换版本</a-tag>
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
运行时间:
{{ i18n "pages.index.operationHours" }}:
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
<template slot="title">
系统自启动以来的运行时间
{{ i18n "pages.index.operationHoursDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -94,15 +94,15 @@
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
系统负载: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
tcp / udp 连接数: [[ status.tcpCount ]] / [[ status.udpCount ]]
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
所有网卡的总连接数
{{ i18n "pages.index.connectionCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -116,7 +116,7 @@
[[ sizeFormat(status.netIO.up) ]] / S
<a-tooltip>
<template slot="title">
所有网卡的总上传速度
{{ i18n "pages.index.upSpeed" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -126,7 +126,7 @@
[[ sizeFormat(status.netIO.down) ]] / S
<a-tooltip>
<template slot="title">
所有网卡的总下载速度
{{ i18n "pages.index.downSpeed" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -142,7 +142,7 @@
[[ sizeFormat(status.netTraffic.sent) ]]
<a-tooltip>
<template slot="title">
系统启动以来所有网卡的总上传流量
{{ i18n "pages.index.totalSent" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -152,7 +152,7 @@
[[ sizeFormat(status.netTraffic.recv) ]]
<a-tooltip>
<template slot="title">
系统启动以来所有网卡的总下载流量
{{ i18n "pages.index.totalReceive" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -164,11 +164,11 @@
</transition>
</a-layout-content>
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title="切换版本"
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false"
ok-text="确定" cancel-text="取消">
<h2>点击你想切换的版本</h2>
<h2>请谨慎选择,旧版本可能配置不兼容</h2>
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'blue' : 'green'"
style="margin: 10px" @click="switchV2rayVersion(version)">
@@ -277,10 +277,10 @@
status: new Status(),
versionModal,
spinning: false,
loadingTip: '加载中',
loadingTip: '{{ i18n "loading"}}',
},
methods: {
loading(spinning, tip = '加载中') {
loading(spinning, tip = '{{ i18n "loading"}}') {
this.spinning = spinning;
this.loadingTip = tip;
},
@@ -304,13 +304,13 @@
},
switchV2rayVersion(version) {
this.$confirm({
title: '切换 xray 版本',
content: '是否切换 xray 版本至' + ` ${version}?`,
okText: '确定',
cancelText: '取消',
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
this.loading(true, '安装中,请不要刷新此页面');
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
await HttpUtil.post(`/server/installXray/${version}`);
this.loading(false);
},

View File

@@ -32,56 +32,82 @@
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">保存配置</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">重启面板</a-button>
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="面板配置">
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP重启面板生效" v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title="面板监听端口" desc="重启面板生效" v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾,重启面板生效" v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"/>
</a-col>
<a-col :lg="24" :xl="12">
<temlate>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
style="width: 100%"
>
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</temlate>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab="用户设置">
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<a-form style="background: white; padding: 20px">
<a-form-item label="原用户名">
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label="原密码">
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
<a-input type="password" v-model="user.oldPassword"
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label="新用户名">
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label="新密码">
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
<a-input type="password" v-model="user.newPassword"
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="updateUser">修改</a-button>
<!-- <a-button type="primary" @click="updateUser">修改</a-button>-->
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab="xray 相关设置">
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab="TG提醒相关设置">
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="switch" title="启用电报机器人" desc="重启面板生效" v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title="电报机器人TOKEN" desc="重启面板生效" v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="number" title="电报机器人ChatId" desc="重启面板生效" v-model.number="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title="电报机器人通知时间" desc="采用Crontab定时格式,重启面板生效" v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="5" tab="其他设置">
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
@@ -104,6 +130,7 @@
allSetting: new AllSetting(),
saveBtnDisable: true,
user: {},
lang : getLang()
},
methods: {
loading(spinning = true) {
@@ -138,10 +165,10 @@
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '重启面板',
content: '确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息',
okText: '确定',
cancelText: '取消',
title: '{{ i18n "pages.setting.restartPanel" }}',
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});

View File

@@ -9,4 +9,164 @@
"download" = "download"
"remark" = "remark"
"enable" = "enable"
"protocol" = "protocol"
"protocol" = "protocol"
"loading" = "Loading"
"second" = "second"
"minute" = "minute"
"hour" = "hour"
"day" = "day"
"check" = "check"
"indefinitely" = "indefinitely"
"unlimited" = "unlimited"
"none" = "none"
"qrCode" = "QR Code"
"edit" = "edit"
"delete" = "delete"
"reset" = "reset"
"copySuccess" = "Copy successfully"
"sure" = "Sure"
"encryption" = "encryption"
"transmission" = "transmission"
"host" = "host"
"path" = "path"
"camouflage" = "camouflage"
"turnOn" = "turn on"
"closure" = "closure"
"domainName" = "domain name"
"additional" = "additional"
"monitor" = "monitor"
"certificate" = "certificat"
"fail" = "fail"
"success" = "success"
"getVersion" = "get version"
"install" = "install"
[menu]
"dashboard" = "System Status"
"inbounds" = "Inbounds"
"setting" = "Panel Setting"
"logout" = "LogOut"
"link" = "Other"
[pages.login]
"title" = "Login"
"loginAgain" = "The login time limit has expired, please log in again"
[pages.login.toasts]
"invalidFormData" = "Input Data Format Is Invalid"
"emptyUsername" = "please Enter Username"
"emptyPassword" = "please Enter Password"
"wrongUsernameOrPassword" = "invalid username or password"
"successLogin" = "Login"
[pages.index]
"title" = "system status"
"memory" = "memory"
"hard" = "hard disk"
"xrayStatus" = "xray Status"
"xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch"
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
"operationHours" = "Operation Hours"
"operationHoursDesc" = "The running time of the system since it was started"
"systemLoad" = "System Load"
"connectionCount" = "Connection Count"
"connectionCountDesc" = "The total number of connections for all network cards"
"upSpeed" = "Total upload speed for all network cards"
"downSpeed" = "Total download speed for all network cards"
"totalSent" = "Total upload traffic of all network cards since system startup"
"totalReceive" = "Total download traffic of all network cards since system startup"
"xraySwitchVersionDialog" = "switch xray version"
"xraySwitchVersionDialogDesc" = "whether to switch the xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
[pages.inbounds]
"title" = "Inbounds"
"totalDownUp" = "Total uploads/downloads"
"totalUsage" = "Total usage"
"inboundCount" = "Number of inbound"
"operate" = "operate"
"enable" = "enable"
"remark" = "remark"
"protocol" = "protocol"
"port" = "port"
"traffic" = "traffic"
"details" = "details"
"transportConfig" = "transport config"
"expireDate" = "expire date"
"resetTraffic" = "reset traffic"
"addInbound" = "addInbound"
"addTo" = "Add To"
"revise" = "Revise"
"modifyInbound" = "Modify InBound"
"deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?"
"resetTrafficContent" = "Are you sure you want to reset traffic?"
"copyLink" = "Copy Link"
"address" = "address"
"network" = "network"
"destinationPort" = "destination port"
"targetAddress" = "target address"
"disableInsecureEncryption" = "Disable insecure encryption"
"monitorDesc" = "Leave blank by default"
"meansNoLimit" = "means no limit"
"totalFlow" = "total flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
"certificatePath" = "certificate file path"
"certificateContent" = "certificate file content"
"publicKeyPath" = "public key file path"
"publicKeyContent" = "public key content"
"keyPath" = "key file path"
"keyContent" = "key content"
[pages.inbounds.toasts]
"obtain" = "Obtain"
[pages.setting]
"title" = "Setting"
"save" = "Save"
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
"panelConfig" = "Panel Configuration"
"userSetting" = "User Setting"
"xrayConfiguration" = "xray Configuration"
"TGReminder" = "TG Reminder Related Settings"
"otherSetting" = "Other Setting"
"panelListeningIP" = "Panel listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
"panelPort" = "Panel Port"
"panelPortDesc" = "Restart the panel to take effect"
"publicKeyPath" = "Panel certificate public key file path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
"privateKeyPath" = "Panel certificate key file path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
"panelUrlPath" = "panel url root path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
"newUsername" = "New Username"
"newPassword" = "New Password"
"xrayConfigTemplate" = "xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect"
"telegramBotEnable" = "Enable telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram ChatId"
"telegramChatIdDesc" = "Restart the panel to take effect"
"telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
"timeZonee" = "Time Zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
[pages.setting.toasts]
"modifySetting" = "modify setting"
"getSetting" = "get setting"
"modifyUser" = "modify user"
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"

View File

@@ -9,4 +9,165 @@
"download" = "下载"
"remark" = "备注"
"enable" = "启用"
"protocol" = "协议"
"protocol" = "协议"
"loading" = "加载中"
"second" = "秒"
"minute" = "分钟"
"hour" = "小时"
"day" = "天"
"check" = "查看"
"indefinitely" = "无限期"
"unlimited" = "无限制"
"none" = "无"
"qrCode" = "二维码"
"edit" = "编辑"
"delete" = "删除"
"reset" = "重置"
"copySuccess" = "复制成功"
"sure" = "确定"
"encryption" = "加密"
"transmission" = "传输"
"host" = "主持人"
"path" = "小路"
"camouflage" = "伪装"
"turnOn" = "开启"
"closure" = "关闭"
"domainName" = "域名"
"additional" = "额外"
"monitor" = "监听"
"certificate" = "证书"
"fail" = "失败"
"success" = "成功"
"getVersion" = "获取版本"
"install" = "安装"
[menu]
"dashboard" = "系统状态"
"inbounds" = "入站列表"
"setting" = "面板设置"
"logout" = "退出登录"
"link" = "其他"
[pages.login]
"title" = "登录"
"loginAgain" = "登录时效已过,请重新登录"
[pages.login.toasts]
"invalidFormData" = "数据格式错误"
"emptyUsername" = "请输入用户名"
"emptyPassword" = "请输入密码"
"wrongUsernameOrPassword" = "用户名或密码错误"
"successLogin" = "登录"
[pages.index]
"title" = "系统状态"
"memory" = "内存"
"hard" = "硬盘"
"xrayStatus" = "xray 状态"
"xraySwitch" = "切换版本"
"xraySwitchClick" = "点击你想切换的版本"
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
"operationHours" = "运行时间"
"operationHoursDesc" = "系统自启动以来的运行时间"
"systemLoad" = "系统负载"
"connectionCount" = "连接数"
"connectionCountDesc" = "所有网卡的总连接数"
"upSpeed" = "所有网卡的总上传速度"
"downSpeed" = "所有网卡的总下载速度"
"totalSent" = "系统启动以来所有网卡的总上传流量"
"totalReceive" = "系统启动以来所有网卡的总下载流量"
"xraySwitchVersionDialog" = "切换 xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面"
[pages.inbounds]
"title" = "入站列表"
"totalDownUp" = "总上传 / 下载"
"totalUsage" = "总用量"
"inboundCount" = "入站数量"
"operate" = "操作"
"enable" = "启用"
"remark" = "备注"
"protocol" = "协议"
"port" = "端口"
"traffic" = "流量"
"details" = "详细信息"
"transportConfig" = "传输配置"
"expireDate" = "到期时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入"
"addTo" = "添加"
"revise" = "修改"
"modifyInbound" = "修改入站"
"deleteInbound" = "删除入站"
"deleteInboundContent" = "确定要删除入站吗?"
"resetTrafficContent" = "确定要重置流量吗?"
"copyLink" = "复制链接"
"address" = "地址"
"network" = "网络"
"destinationPort" = "目标端口"
"targetAddress" = "目标地址"
"disableInsecureEncryption" = "禁用不安全加密"
"monitorDesc" = "默认留空即可"
"meansNoLimit" = "表示不限制"
"totalFlow" = "总流量"
"leaveBlankToNeverExpire" = "留空则永不到期"
"noRecommendKeepDefault" = "没有特殊需求保持默认即可"
"certificatePath" = "证书文件路径"
"certificateContent" = "证书文件内容"
"publicKeyPath" = "公钥文件路径"
"publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径"
"keyContent" = "密钥内容"
[pages.inbounds.toasts]
"obtain" = "获取"
[pages.setting]
"title" = "设置"
"save" = "保存配置"
"restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"panelConfig" = "面板配置"
"userSetting" = "用户设置"
"xrayConfiguration" = "xray 相关设置"
"TGReminder" = "TG提醒相关设置"
"otherSetting" = "其他设置"
"panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP重启面板生效"
"panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径"
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
"privateKeyPath" = "面板证书密钥文件路径"
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
"panelUrlPath" = "面板 url 根路径"
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效"
"oldUsername" = "原用户名"
"currentPassword" = "原密码"
"newUsername" = "新用户名"
"newPassword" = "新密码"
"xrayConfigTemplate" = "xray 配置模版"
"xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效"
"telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "电报机器人ChatId"
"telegramChatIdDesc" = "重启面板生效"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"timeZonee" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
[pages.setting.toasts]
"modifySetting" = "修改设置"
"getSetting" = "获取设置"
"modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"

View File

@@ -9,4 +9,30 @@
"download" = "下載"
"remark" = "備註"
"enable" = "啟用"
"protocol" = "協議"
"protocol" = "協議"
[menu]
"dashboard" = "系统状态"
"inbounds" = "入站列表"
"setting" = "面板设置"
"logout" = "退出登录"
"link" = "其他"
[pages.login]
"title" = "登錄"
[pages.login.toasts]
"invalidFormData" = "数据格式错误"
"emptyUsername" = "请输入用户名"
"emptyPassword" = "请输入密码"
"wrongUsernameOrPassword" = "用户名或密码错误"
"successLogin" = "登录"
[pages.index]
"title" = "系统状态"
[pages.inbounds]
"title" = "入站列表"
[pages.setting]
"title" = "设置"

View File

@@ -254,7 +254,7 @@ func (s *Server) initI18n(engine *gin.Engine) error {
var localizer *i18n.Localizer
engine.FuncMap["i18n"] = func(key string, params ...string) (string, error) {
I18n := func(key string, params ...string) (string, error) {
names := findI18nParamNames(key)
if len(names) != len(params) {
return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal")
@@ -269,10 +269,22 @@ func (s *Server) initI18n(engine *gin.Engine) error {
})
}
engine.FuncMap["i18n"] = I18n;
engine.Use(func(c *gin.Context) {
accept := c.GetHeader("Accept-Language")
localizer = i18n.NewLocalizer(bundle, accept)
//accept := c.GetHeader("Accept-Language")
var lang string
if cookie, err := c.Request.Cookie("lang"); err == nil {
lang = cookie.Value
} else {
lang = c.GetHeader("Accept-Language")
}
localizer = i18n.NewLocalizer(bundle, lang)
c.Set("localizer", localizer)
c.Set("I18n" , I18n)
c.Next()
})