Merge pull request #116 from xeefei/3x-ui

3X UI
This commit is contained in:
心隨緣動 2025-08-05 08:08:11 +08:00 committed by GitHub
commit 5e5d09d0dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 210 additions and 30 deletions

View File

@ -1 +1 @@
2.6.3 2.6.5

View File

@ -40,21 +40,22 @@ arch() {
esac esac
} }
echo "Arch: $(arch)"
echo "" echo ""
check_glibc_version() { # check_glibc_version() {
glibc_version=$(ldd --version | head -n1 | awk '{print $NF}') # glibc_version=$(ldd --version | head -n1 | awk '{print $NF}')
required_version="2.32" # required_version="2.32"
if [[ "$(printf '%s\n' "$required_version" "$glibc_version" | sort -V | head -n1)" != "$required_version" ]]; then # if [[ "$(printf '%s\n' "$required_version" "$glibc_version" | sort -V | head -n1)" != "$required_version" ]]; then
echo -e "${red}------>>>GLIBC版本 $glibc_version 太旧了! 要求2.32或以上版本${plain}" # echo -e "${red}------>>>GLIBC版本 $glibc_version 太旧了! 要求2.32或以上版本${plain}"
echo -e "${green}-------->>>>请升级到较新版本的操作系统以便获取更高版本的GLIBC${plain}" # echo -e "${green}-------->>>>请升级到较新版本的操作系统以便获取更高版本的GLIBC${plain}"
exit 1 # exit 1
fi # fi
echo -e "${green}-------->>>>GLIBC版本 $glibc_version符合高于2.32的要求)${plain}" # echo -e "${green}-------->>>>GLIBC版本 $glibc_version符合高于2.32的要求)${plain}"
} # }
check_glibc_version # check_glibc_version
echo "" # echo ""
echo -e "${yellow}---------->>>>>当前系统的架构为: $(arch)${plain}" echo -e "${yellow}---------->>>>>当前系统的架构为: $(arch)${plain}"
echo "" echo ""
last_version=$(curl -Ls "https://api.github.com/repos/xeefei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') last_version=$(curl -Ls "https://api.github.com/repos/xeefei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')

View File

@ -2312,12 +2312,14 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
protocol, protocol,
address, address,
port, port,
portMap = [],
network = 'tcp,udp', network = 'tcp,udp',
followRedirect = false followRedirect = false
) { ) {
super(protocol); super(protocol);
this.address = address; this.address = address;
this.port = port; this.port = port;
this.portMap = portMap;
this.network = network; this.network = network;
this.followRedirect = followRedirect; this.followRedirect = followRedirect;
} }
@ -2327,6 +2329,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
Protocols.DOKODEMO, Protocols.DOKODEMO,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap),
json.network, json.network,
json.followRedirect, json.followRedirect,
); );
@ -2336,6 +2339,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
return { return {
address: this.address, address: this.address,
port: this.port, port: this.port,
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
network: this.network, network: this.network,
followRedirect: this.followRedirect, followRedirect: this.followRedirect,
}; };

View File

@ -108,8 +108,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -126,8 +126,8 @@ func (a *InboundController) delInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -152,8 +152,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -342,25 +342,25 @@ func (a *InboundController) onlines(c *gin.Context) {
func (a *InboundController) updateClientTraffic(c *gin.Context) { func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
// Define the request structure for traffic update // Define the request structure for traffic update
type TrafficUpdateRequest struct { type TrafficUpdateRequest struct {
Upload int64 `json:"upload"` Upload int64 `json:"upload"`
Download int64 `json:"download"` Download int64 `json:"download"`
} }
var request TrafficUpdateRequest var request TrafficUpdateRequest
err := c.ShouldBindJSON(&request) err := c.ShouldBindJSON(&request)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
return return
} }
err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download) err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
} }

View File

@ -46,6 +46,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb) g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
@ -134,6 +135,12 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
func (a *ServerController) getXrayLogs(c *gin.Context) {
count := c.Param("count")
logs := a.serverService.GetXrayLogs(count)
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {

View File

@ -44,7 +44,7 @@
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable"> <a-form-item v-if="client.email && app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View File

@ -6,6 +6,19 @@
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'> <a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model.number="inbound.settings.port"></a-input-number> <a-input-number v-model.number="inbound.settings.port"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.portMap"}}'>
<a-button size="small" @click="inbound.settings.portMap.push({name: '', value: ''})">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(pm, index) in inbound.settings.portMap">
<a-input style="width: 50%" v-model.trim="pm.name" placeholder='{{ i18n "pages.inbounds.port"}}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="pm.value" placeholder='{{ i18n "pages.inbounds.targetAddress" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.settings.portMap.splice(index,1)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">TCP,UDP</a-select-option> <a-select-option value="tcp,udp">TCP,UDP</a-select-option>

View File

@ -167,7 +167,10 @@
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col> </a-col>
<a-col> <a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag> <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
</a-col>
<a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openXrayLogs()"></a-icon>
</a-col> </a-col>
</a-row> </a-row>
</span> </span>
@ -179,6 +182,10 @@
</template> </template>
</template> </template>
<template #actions> <template #actions>
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="poweroff"></a-icon> <a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
@ -424,6 +431,40 @@
</a-form> </a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div> <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
</a-modal> </a-modal>
<a-modal id="xraylog-modal"
v-model="xraylogModal.visible"
:closable="true" @cancel="() => xraylogModal.visible = false"
:class="themeSwitcher.currentTheme"
width="80vw"
footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="xraylogModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="xraylogModal.loading"
@click="openXrayLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
</a-modal>
<a-modal id="backup-modal" <a-modal id="backup-modal"
v-model="backupModal.visible" v-model="backupModal.visible"
title='{{ i18n "pages.index.backupTitle" }}' title='{{ i18n "pages.index.backupTitle" }}'
@ -608,6 +649,57 @@
}, },
}; };
const xraylogModal = {
visible: false,
logs: [],
rows: 20,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
logs.forEach((log, index) => {
if(index > 0) formattedLogs += '<br>';
const parts = log.split(' ');
if(parts.length === 9) {
const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
const from = `<b>${parts[3]}</b>`;
const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
let outboundColor = '';
if (parts[8].startsWith('blocked')) {
outboundColor = ' style="color: #e04141;"';
}
else if (!parts[8].startsWith('direct')) {
outboundColor = ' style="color: #3c89e8;"';
}
formattedLogs += `<span${outboundColor}>
${dateTime}
${parts[2]}
${from}
${parts[4]}
${to}
${parts.slice(6).join(' ')}
</span>`;
} else {
formattedLogs += `<span>${parts.join(' ')}</span>`;
}
});
return formattedLogs;
},
hide() {
this.visible = false;
},
};
const backupModal = { const backupModal = {
visible: false, visible: false,
show() { show() {
@ -631,10 +723,12 @@
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
xraylogModal,
backupModal, backupModal,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false, showAlert: false,
showIp: false showIp: false,
ipLimitEnable: false,
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
@ -723,6 +817,16 @@
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
logModal.loading = false; logModal.loading = false;
}, },
async openXrayLogs(){
xraylogModal.loading = true;
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows);
if (!msg.success) {
return;
}
xraylogModal.show(msg.obj);
await PromiseUtil.sleep(500);
xraylogModal.loading = false;
},
async openConfig() { async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.post('server/getConfigJson');
@ -775,6 +879,12 @@
if (window.location.protocol !== "https:") { if (window.location.protocol !== "https:") {
this.showAlert = true; this.showAlert = true;
} }
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg.success) {
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
while (true) { while (true) {
try { try {
await this.getStatus(); await this.getStatus();

View File

@ -39,7 +39,7 @@
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="app.subSettings.enable"> <a-form-item v-if="app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View File

@ -199,7 +199,7 @@
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag> <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="app.ipLimitEnable"> <tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td> <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td> <td>
<a-tag>[[ infoModal.clientIps ]]</a-tag> <a-tag>[[ infoModal.clientIps ]]</a-tag>

View File

@ -21,7 +21,7 @@
</a-space> </a-space>
</template> </template>
<tr-qr-modal class="qr-modal"> <tr-qr-modal class="qr-modal">
<template v-if="app.subSettings.enable && qrModal.subId"> <template v-if="app.subSettings?.enable && qrModal.subId">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag> <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
<tr-qr-bg class="qr-bg-sub"> <tr-qr-bg class="qr-bg-sub">

View File

@ -2,6 +2,7 @@ package service
import ( import (
"archive/zip" "archive/zip"
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -329,7 +330,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue continue
} }
if major > 25 || (major == 25 && minor > 7) || (major == 25 && minor == 7 && patch >= 26) { if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }
@ -481,6 +482,37 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
return lines return lines
} }
func (s *ServerService) GetXrayLogs(count string) []string {
c, _ := strconv.Atoi(count)
var lines []string
pathToAccessLog, err := xray.GetAccessLogPath()
if err != nil {
return lines
}
file, err := os.Open(pathToAccessLog)
if err != nil {
return lines
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" || strings.Contains(line, "api -> api") {
continue
}
lines = append(lines, line)
}
if len(lines) > c {
lines = lines[len(lines)-c:]
}
return lines
}
func (s *ServerService) GetConfigJson() (any, error) { func (s *ServerService) GetConfigJson() (any, error) {
config, err := s.xrayService.GetXrayConfig() config, err := s.xrayService.GetXrayConfig()
if err != nil { if err != nil {

View File

@ -158,6 +158,7 @@
"remark" = "ملاحظة" "remark" = "ملاحظة"
"protocol" = "بروتوكول" "protocol" = "بروتوكول"
"port" = "بورت" "port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك" "traffic" = "الترافيك"
"details" = "تفاصيل" "details" = "تفاصيل"
"transportConfig" = "نقل" "transportConfig" = "نقل"

View File

@ -159,6 +159,7 @@
"remark" = "Remark" "remark" = "Remark"
"protocol" = "Protocol" "protocol" = "Protocol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport" "transportConfig" = "Transport"

View File

@ -158,6 +158,7 @@
"remark" = "Notas" "remark" = "Notas"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Puerto" "port" = "Puerto"
"portMap" = "Puertos de Destino"
"traffic" = "Tráfico" "traffic" = "Tráfico"
"details" = "Detalles" "details" = "Detalles"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"

View File

@ -158,6 +158,7 @@
"remark" = "نام" "remark" = "نام"
"protocol" = "پروتکل" "protocol" = "پروتکل"
"port" = "پورت" "port" = "پورت"
"portMap" = "پورت‌های نظیر"
"traffic" = "ترافیک" "traffic" = "ترافیک"
"details" = "توضیحات" "details" = "توضیحات"
"transportConfig" = "نحوه اتصال" "transportConfig" = "نحوه اتصال"

View File

@ -158,6 +158,7 @@
"remark" = "Catatan" "remark" = "Catatan"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Rincian" "details" = "Rincian"
"transportConfig" = "Transport" "transportConfig" = "Transport"

View File

@ -158,6 +158,7 @@
"remark" = "備考" "remark" = "備考"
"protocol" = "プロトコル" "protocol" = "プロトコル"
"port" = "ポート" "port" = "ポート"
"portMap" = "ポートマッピング"
"traffic" = "トラフィック" "traffic" = "トラフィック"
"details" = "詳細情報" "details" = "詳細情報"
"transportConfig" = "トランスポート設定" "transportConfig" = "トランスポート設定"

View File

@ -158,6 +158,7 @@
"remark" = "Observação" "remark" = "Observação"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Porta" "port" = "Porta"
"portMap" = "Porta Mapeada"
"traffic" = "Tráfego" "traffic" = "Tráfego"
"details" = "Detalhes" "details" = "Detalhes"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"

View File

@ -158,6 +158,7 @@
"remark" = "Примечание" "remark" = "Примечание"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-маппинг"
"traffic" = "Трафик" "traffic" = "Трафик"
"details" = "Подробнее" "details" = "Подробнее"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"

View File

@ -158,6 +158,7 @@
"remark" = "Açıklama" "remark" = "Açıklama"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Atama"
"traffic" = "Trafik" "traffic" = "Trafik"
"details" = "Detaylar" "details" = "Detaylar"
"transportConfig" = "Taşıma" "transportConfig" = "Taşıma"

View File

@ -158,6 +158,7 @@
"remark" = "Примітка" "remark" = "Примітка"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-перехід"
"traffic" = "Трафік" "traffic" = "Трафік"
"details" = "Деталі" "details" = "Деталі"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"

View File

@ -158,6 +158,7 @@
"remark" = "Chú thích" "remark" = "Chú thích"
"protocol" = "Giao thức" "protocol" = "Giao thức"
"port" = "Cổng" "port" = "Cổng"
"portMap" = "Cổng tạo"
"traffic" = "Lưu lượng" "traffic" = "Lưu lượng"
"details" = "Chi tiết" "details" = "Chi tiết"
"transportConfig" = "Giao vận" "transportConfig" = "Giao vận"

View File

@ -161,6 +161,7 @@
"remark" = "备注" "remark" = "备注"
"protocol" = "协议" "protocol" = "协议"
"port" = "端口" "port" = "端口"
"portMap" = "端口映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "详细信息" "details" = "详细信息"
"transportConfig" = "传输配置" "transportConfig" = "传输配置"

View File

@ -158,6 +158,7 @@
"remark" = "備註" "remark" = "備註"
"protocol" = "協議" "protocol" = "協議"
"port" = "埠" "port" = "埠"
"portMap" = "埠映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "詳細資訊" "details" = "詳細資訊"
"transportConfig" = "傳輸配置" "transportConfig" = "傳輸配置"