mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-27 19:50:56 +00:00
Merge branch 'main' into feat/wireguard-clients
This commit is contained in:
commit
d2b6bedc47
21 changed files with 283 additions and 42 deletions
|
|
@ -6850,6 +6850,55 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/groups/resetTraffic": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Reset only the group-level traffic counter shown on the groups page. Snapshots the current up/down sum of the group's members as a baseline so the group total reads zero, while leaving each client's own counters (and their quotas) untouched. No Xray restart is triggered. Creates the client_groups row if the group exists only as a derived label.",
|
||||
"operationId": "post_panel_api_clients_groups_resetTraffic",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"name": "customer-a"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"name": "customer-a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/resetTraffic/{email}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
|
|
|||
|
|
@ -815,6 +815,13 @@ export const sections: readonly Section[] = [
|
|||
body: '{\n "name": "customer-a"\n}',
|
||||
response: '{\n "success": true,\n "obj": {\n "affected": 5\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/clients/groups/resetTraffic',
|
||||
summary: 'Reset only the group-level traffic counter shown on the groups page. Snapshots the current up/down sum of the group\'s members as a baseline so the group total reads zero, while leaving each client\'s own counters (and their quotas) untouched. No Xray restart is triggered. Creates the client_groups row if the group exists only as a derived label.',
|
||||
body: '{\n "name": "customer-a"\n}',
|
||||
response: '{\n "success": true,\n "obj": {\n "name": "customer-a"\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/clients/resetTraffic/:email',
|
||||
|
|
|
|||
|
|
@ -126,9 +126,9 @@ export default function GroupsPage() {
|
|||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
});
|
||||
|
||||
const bulkResetMut = useMutation({
|
||||
mutationFn: (body: { emails: string[] }) =>
|
||||
HttpUtil.post('/panel/api/clients/bulkResetTraffic', body, JSON_HEADERS),
|
||||
const groupResetMut = useMutation({
|
||||
mutationFn: (body: { name: string }) =>
|
||||
HttpUtil.post('/panel/api/clients/groups/resetTraffic', body, JSON_HEADERS),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
});
|
||||
|
||||
|
|
@ -321,17 +321,14 @@ export default function GroupsPage() {
|
|||
}
|
||||
modal.confirm({
|
||||
title: t('pages.groups.resetConfirmTitle', { name: g.name }),
|
||||
content: t('pages.groups.resetConfirmContent', { count: g.clientCount }),
|
||||
content: t('pages.groups.resetConfirmContent'),
|
||||
okText: t('reset'),
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const emails = await fetchEmailsForGroup(g.name);
|
||||
if (emails.length === 0) return;
|
||||
const msg = await bulkResetMut.mutateAsync({ emails });
|
||||
const msg = await groupResetMut.mutateAsync({ name: g.name });
|
||||
if (msg?.success) {
|
||||
const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? emails.length;
|
||||
messageApi.success(t('pages.groups.resetSuccess', { count: affected }));
|
||||
messageApi.success(t('pages.groups.resetSuccess', { name: g.name }));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -649,6 +649,8 @@ func (ClientRecord) TableName() string { return "clients" }
|
|||
type ClientGroup struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;not null"`
|
||||
ResetUp int64 `json:"resetUp" gorm:"column:reset_up;default:0"`
|
||||
ResetDown int64 `json:"resetDown" gorm:"column:reset_down;default:0"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package sys
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
|
@ -93,7 +94,7 @@ func CPUPercentRaw() (float64, error) {
|
|||
|
||||
rd := bufio.NewReader(f)
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, err
|
||||
}
|
||||
// Expect: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ func (a *GroupController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/groups/create", a.create)
|
||||
g.POST("/groups/rename", a.rename)
|
||||
g.POST("/groups/delete", a.delete)
|
||||
g.POST("/groups/resetTraffic", a.resetTraffic)
|
||||
g.POST("/groups/bulkAdd", a.bulkAdd)
|
||||
g.POST("/groups/bulkRemove", a.bulkRemove)
|
||||
}
|
||||
|
|
@ -108,6 +109,24 @@ func (a *GroupController) delete(c *gin.Context) {
|
|||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
type groupResetTrafficBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a *GroupController) resetTraffic(c *gin.Context) {
|
||||
var body groupResetTrafficBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
if err := a.clientService.ResetGroupTraffic(body.Name); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, gin.H{"name": body.Name}, nil)
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
type bulkAddToGroupRequest struct {
|
||||
Emails []string `json:"emails"`
|
||||
Group string `json:"group"`
|
||||
|
|
|
|||
127
internal/web/service/client_group_reset_test.go
Normal file
127
internal/web/service/client_group_reset_test.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
func groupByName(t *testing.T, svc *ClientService, name string) GroupSummary {
|
||||
t.Helper()
|
||||
rows, err := svc.ListGroups()
|
||||
if err != nil {
|
||||
t.Fatalf("ListGroups: %v", err)
|
||||
}
|
||||
for _, g := range rows {
|
||||
if g.Name == name {
|
||||
return g
|
||||
}
|
||||
}
|
||||
t.Fatalf("group %q not found in %v", name, rows)
|
||||
return GroupSummary{}
|
||||
}
|
||||
|
||||
func seedGroupedClient(t *testing.T, email, group string, up, down int64) {
|
||||
t.Helper()
|
||||
if err := database.GetDB().Create(&model.ClientRecord{Email: email, Enable: true, Group: group}).Error; err != nil {
|
||||
t.Fatalf("seed client record %q: %v", email, err)
|
||||
}
|
||||
seedClientRow(t, email, 1, up, down, 0)
|
||||
}
|
||||
|
||||
func TestResetGroupTraffic_ZeroesGroupButKeepsClients(t *testing.T) {
|
||||
initTrafficTestDB(t)
|
||||
svc := &ClientService{}
|
||||
|
||||
seedGroupedClient(t, "alice", "vip", 100, 200)
|
||||
seedGroupedClient(t, "bob", "vip", 50, 50)
|
||||
|
||||
before := groupByName(t, svc, "vip")
|
||||
if before.Up != 150 || before.Down != 250 || before.TrafficUsed != 400 || before.ClientCount != 2 {
|
||||
t.Fatalf("before reset: got %+v, want up=150 down=250 used=400 count=2", before)
|
||||
}
|
||||
|
||||
if err := svc.ResetGroupTraffic("vip"); err != nil {
|
||||
t.Fatalf("ResetGroupTraffic: %v", err)
|
||||
}
|
||||
|
||||
after := groupByName(t, svc, "vip")
|
||||
if after.Up != 0 || after.Down != 0 || after.TrafficUsed != 0 {
|
||||
t.Fatalf("after reset: got %+v, want up=0 down=0 used=0", after)
|
||||
}
|
||||
if after.ClientCount != 2 {
|
||||
t.Fatalf("after reset: client count changed to %d, want 2", after.ClientCount)
|
||||
}
|
||||
|
||||
var alice xray.ClientTraffic
|
||||
if err := database.GetDB().Where("email = ?", "alice").First(&alice).Error; err != nil {
|
||||
t.Fatalf("load alice traffic: %v", err)
|
||||
}
|
||||
if alice.Up != 100 || alice.Down != 200 {
|
||||
t.Fatalf("client counter modified by group reset: alice up=%d down=%d, want 100/200", alice.Up, alice.Down)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetGroupTraffic_NewTrafficAccumulatesAboveBaseline(t *testing.T) {
|
||||
initTrafficTestDB(t)
|
||||
svc := &ClientService{}
|
||||
|
||||
seedGroupedClient(t, "carol", "team", 100, 100)
|
||||
if err := svc.ResetGroupTraffic("team"); err != nil {
|
||||
t.Fatalf("ResetGroupTraffic: %v", err)
|
||||
}
|
||||
if g := groupByName(t, svc, "team"); g.Up != 0 || g.Down != 0 {
|
||||
t.Fatalf("after reset: got %+v, want up=0 down=0", g)
|
||||
}
|
||||
|
||||
if err := database.GetDB().Table("client_traffics").
|
||||
Where("email = ?", "carol").
|
||||
Updates(map[string]any{"up": 130, "down": 100}).Error; err != nil {
|
||||
t.Fatalf("bump carol traffic: %v", err)
|
||||
}
|
||||
|
||||
g := groupByName(t, svc, "team")
|
||||
if g.Up != 30 || g.Down != 0 || g.TrafficUsed != 30 {
|
||||
t.Fatalf("post-bump: got %+v, want up=30 down=0 used=30", g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetGroupTraffic_CreatesRowForDerivedGroup(t *testing.T) {
|
||||
initTrafficTestDB(t)
|
||||
svc := &ClientService{}
|
||||
|
||||
seedGroupedClient(t, "dave", "adhoc", 70, 30)
|
||||
|
||||
var rows int64
|
||||
if err := database.GetDB().Model(&model.ClientGroup{}).Where("name = ?", "adhoc").Count(&rows).Error; err != nil {
|
||||
t.Fatalf("count client_groups: %v", err)
|
||||
}
|
||||
if rows != 0 {
|
||||
t.Fatalf("precondition: derived group should have no client_groups row, got %d", rows)
|
||||
}
|
||||
|
||||
if err := svc.ResetGroupTraffic("adhoc"); err != nil {
|
||||
t.Fatalf("ResetGroupTraffic: %v", err)
|
||||
}
|
||||
|
||||
var stored model.ClientGroup
|
||||
if err := database.GetDB().Where("name = ?", "adhoc").First(&stored).Error; err != nil {
|
||||
t.Fatalf("client_groups row not created: %v", err)
|
||||
}
|
||||
if stored.ResetUp != 70 || stored.ResetDown != 30 {
|
||||
t.Fatalf("baseline not snapshotted: got up=%d down=%d, want 70/30", stored.ResetUp, stored.ResetDown)
|
||||
}
|
||||
if g := groupByName(t, svc, "adhoc"); g.Up != 0 || g.Down != 0 {
|
||||
t.Fatalf("after reset: got %+v, want up=0 down=0", g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetGroupTraffic_EmptyNameRejected(t *testing.T) {
|
||||
initTrafficTestDB(t)
|
||||
svc := &ClientService{}
|
||||
if err := svc.ResetGroupTraffic(" "); err == nil {
|
||||
t.Fatal("ResetGroupTraffic(blank) = nil, want error")
|
||||
}
|
||||
}
|
||||
|
|
@ -36,21 +36,32 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
|||
return nil, err
|
||||
}
|
||||
type groupAgg struct {
|
||||
count int
|
||||
traffic int64
|
||||
up int64
|
||||
down int64
|
||||
count int
|
||||
up int64
|
||||
down int64
|
||||
}
|
||||
baseUp := make(map[string]int64, len(stored))
|
||||
baseDown := make(map[string]int64, len(stored))
|
||||
merged := make(map[string]groupAgg, len(derived)+len(stored))
|
||||
for _, g := range stored {
|
||||
merged[g.Name] = groupAgg{}
|
||||
baseUp[g.Name] = g.ResetUp
|
||||
baseDown[g.Name] = g.ResetDown
|
||||
}
|
||||
for _, g := range derived {
|
||||
merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed, up: g.Up, down: g.Down}
|
||||
merged[g.Name] = groupAgg{count: g.ClientCount, up: g.Up, down: g.Down}
|
||||
}
|
||||
out := make([]GroupSummary, 0, len(merged))
|
||||
for name, agg := range merged {
|
||||
out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic, Up: agg.up, Down: agg.down})
|
||||
up := agg.up - baseUp[name]
|
||||
if up < 0 {
|
||||
up = 0
|
||||
}
|
||||
down := agg.down - baseDown[name]
|
||||
if down < 0 {
|
||||
down = 0
|
||||
}
|
||||
out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: up + down, Up: up, Down: down})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||
|
|
@ -77,6 +88,34 @@ func (s *ClientService) EmailsByGroup(name string) ([]string, error) {
|
|||
return emails, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) ResetGroupTraffic(name string) error {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return common.NewError("group name is required")
|
||||
}
|
||||
db := database.GetDB()
|
||||
var agg struct {
|
||||
Up int64
|
||||
Down int64
|
||||
}
|
||||
if err := db.Table("clients AS c").
|
||||
Select("COALESCE(SUM(ct.up), 0) AS up, COALESCE(SUM(ct.down), 0) AS down").
|
||||
Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
|
||||
Where("c.group_name = ?", name).
|
||||
Scan(&agg).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&model.ClientGroup{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return db.Create(&model.ClientGroup{Name: name, ResetUp: agg.Up, ResetDown: agg.Down}).Error
|
||||
}
|
||||
return db.Model(&model.ClientGroup{}).Where("name = ?", name).
|
||||
Updates(map[string]any{"reset_up": agg.Up, "reset_down": agg.Down}).Error
|
||||
}
|
||||
|
||||
func (s *ClientService) CreateGroup(name string) error {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "تم مسح المجموعة من {count} عميل.",
|
||||
"resetTraffic": "إعادة تعيين حركة المرور",
|
||||
"resetConfirmTitle": "إعادة تعيين حركة المرور للمجموعة {name}؟",
|
||||
"resetConfirmContent": "يصفر up/down لجميع {count} عميل في هذه المجموعة.",
|
||||
"resetSuccess": "تمت إعادة تعيين حركة المرور لـ {count} عميل.",
|
||||
"resetConfirmContent": "يعيد تعيين عداد حركة مرور المجموعة فقط؛ ولا تتأثر عدادات العملاء الفرديين.",
|
||||
"resetSuccess": "تمت إعادة تعيين حركة مرور المجموعة {name}.",
|
||||
"adjustSuccess": "تم ضبط {count} عميل في {name}.",
|
||||
"emptyForAction": "هذه المجموعة فارغة.",
|
||||
"deleteGroupOnly": "حذف المجموعة (مع الاحتفاظ بالعملاء)",
|
||||
|
|
|
|||
|
|
@ -949,8 +949,8 @@
|
|||
"deleteSuccess": "Cleared group from {count} client(s).",
|
||||
"resetTraffic": "Reset traffic",
|
||||
"resetConfirmTitle": "Reset traffic for group {name}?",
|
||||
"resetConfirmContent": "This zeros up/down for all {count} client(s) in this group.",
|
||||
"resetSuccess": "Reset traffic for {count} client(s).",
|
||||
"resetConfirmContent": "This resets only the group's traffic counter. Individual client counters are not affected.",
|
||||
"resetSuccess": "Group {name} traffic reset.",
|
||||
"adjustSuccess": "Adjusted {count} client(s) in {name}.",
|
||||
"emptyForAction": "This group has no clients yet.",
|
||||
"deleteGroupOnly": "Delete group (keep clients)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "Grupo limpiado de {count} cliente(s).",
|
||||
"resetTraffic": "Restablecer tráfico",
|
||||
"resetConfirmTitle": "¿Restablecer tráfico del grupo {name}?",
|
||||
"resetConfirmContent": "Esto pone a cero up/down para los {count} cliente(s) de este grupo.",
|
||||
"resetSuccess": "Tráfico restablecido en {count} cliente(s).",
|
||||
"resetConfirmContent": "Esto restablece solo el contador de tráfico del grupo. Los contadores de cada cliente no se ven afectados.",
|
||||
"resetSuccess": "Tráfico del grupo {name} restablecido.",
|
||||
"adjustSuccess": "Ajustados {count} cliente(s) en {name}.",
|
||||
"emptyForAction": "Este grupo aún no tiene clientes.",
|
||||
"deleteGroupOnly": "Eliminar grupo (conservar clientes)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "گروه از {count} کاربر پاک شد.",
|
||||
"resetTraffic": "بازنشانی ترافیک",
|
||||
"resetConfirmTitle": "بازنشانی ترافیک گروه {name}؟",
|
||||
"resetConfirmContent": "این عمل آپلود/دانلود تمام {count} کاربر این گروه را صفر میکند.",
|
||||
"resetSuccess": "ترافیک {count} کاربر بازنشانی شد.",
|
||||
"resetConfirmContent": "این فقط شمارندهی ترافیک گروه را صفر میکند؛ شمارندهی تکتک کاربران دستنخورده میماند.",
|
||||
"resetSuccess": "ترافیک گروه {name} صفر شد.",
|
||||
"adjustSuccess": "{count} کاربر در {name} تنظیم شد.",
|
||||
"emptyForAction": "این گروه هنوز کاربری ندارد.",
|
||||
"deleteGroupOnly": "حذف گروه (نگه داشتن کاربران)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "Grup dihapus dari {count} klien.",
|
||||
"resetTraffic": "Reset trafik",
|
||||
"resetConfirmTitle": "Reset trafik grup {name}?",
|
||||
"resetConfirmContent": "Ini mengatur ulang up/down ke 0 untuk semua {count} klien di grup ini.",
|
||||
"resetSuccess": "Trafik direset untuk {count} klien.",
|
||||
"resetConfirmContent": "Ini hanya mengatur ulang penghitung trafik grup. Penghitung tiap klien tidak terpengaruh.",
|
||||
"resetSuccess": "Trafik grup {name} direset.",
|
||||
"adjustSuccess": "{count} klien di {name} disesuaikan.",
|
||||
"emptyForAction": "Grup ini belum memiliki klien.",
|
||||
"deleteGroupOnly": "Hapus grup (pertahankan klien)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "{count} クライアントのグループをクリアしました。",
|
||||
"resetTraffic": "トラフィックをリセット",
|
||||
"resetConfirmTitle": "グループ {name} のトラフィックをリセット?",
|
||||
"resetConfirmContent": "このグループ内のすべての {count} クライアントの up/down をゼロにします。",
|
||||
"resetSuccess": "{count} クライアントのトラフィックをリセットしました。",
|
||||
"resetConfirmContent": "グループのトラフィックカウンターのみをリセットします。個々のクライアントのカウンターには影響しません。",
|
||||
"resetSuccess": "グループ {name} のトラフィックをリセットしました。",
|
||||
"adjustSuccess": "{name} 内の {count} クライアントを調整しました。",
|
||||
"emptyForAction": "このグループにはまだクライアントがありません。",
|
||||
"deleteGroupOnly": "グループ削除 (クライアントは保持)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "Grupo limpo de {count} cliente(s).",
|
||||
"resetTraffic": "Redefinir tráfego",
|
||||
"resetConfirmTitle": "Redefinir tráfego do grupo {name}?",
|
||||
"resetConfirmContent": "Isso zera up/down para todos os {count} cliente(s) deste grupo.",
|
||||
"resetSuccess": "Tráfego redefinido para {count} cliente(s).",
|
||||
"resetConfirmContent": "Isso redefine apenas o contador de tráfego do grupo. Os contadores de cada cliente não são afetados.",
|
||||
"resetSuccess": "Tráfego do grupo {name} redefinido.",
|
||||
"adjustSuccess": "Ajustados {count} cliente(s) em {name}.",
|
||||
"emptyForAction": "Este grupo ainda não tem clientes.",
|
||||
"deleteGroupOnly": "Excluir grupo (manter clientes)",
|
||||
|
|
|
|||
|
|
@ -946,8 +946,8 @@
|
|||
"deleteSuccess": "Группа очищена у {count} клиент(ов).",
|
||||
"resetTraffic": "Сбросить трафик",
|
||||
"resetConfirmTitle": "Сбросить трафик группы {name}?",
|
||||
"resetConfirmContent": "Это обнулит up/down для всех {count} клиент(ов) в этой группе.",
|
||||
"resetSuccess": "Сброшен трафик у {count} клиент(ов).",
|
||||
"resetConfirmContent": "Это сбросит только счётчик трафика группы. Счётчики отдельных клиентов не затрагиваются.",
|
||||
"resetSuccess": "Трафик группы {name} сброшен.",
|
||||
"adjustSuccess": "Скорректировано {count} клиент(ов) в {name}.",
|
||||
"emptyForAction": "В этой группе пока нет клиентов.",
|
||||
"deleteGroupOnly": "Удалить группу (сохранить клиентов)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "{count} kullanıcının grubu temizlendi.",
|
||||
"resetTraffic": "Trafiği Sıfırla",
|
||||
"resetConfirmTitle": "{name} Grubunun Trafiğini Sıfırla?",
|
||||
"resetConfirmContent": "Bu, bu gruptaki tüm {count} kullanıcının yukarı/aşağı trafiğini sıfırlar.",
|
||||
"resetSuccess": "{count} kullanıcının trafiği sıfırlandı.",
|
||||
"resetConfirmContent": "Bu yalnızca grubun trafik sayacını sıfırlar. Tek tek kullanıcı sayaçları etkilenmez.",
|
||||
"resetSuccess": "{name} grubunun trafiği sıfırlandı.",
|
||||
"adjustSuccess": "{name} içinde {count} kullanıcı ayarlandı.",
|
||||
"emptyForAction": "Bu grupta henüz kullanıcı yok.",
|
||||
"deleteGroupOnly": "Grubu Sil (Kullanıcıları Tut)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "Групу очищено у {count} клієнт(ів).",
|
||||
"resetTraffic": "Скинути трафік",
|
||||
"resetConfirmTitle": "Скинути трафік групи {name}?",
|
||||
"resetConfirmContent": "Це обнулить up/down для всіх {count} клієнт(ів) у цій групі.",
|
||||
"resetSuccess": "Скинуто трафік у {count} клієнт(ів).",
|
||||
"resetConfirmContent": "Це скине лише лічильник трафіку групи. Лічильники окремих клієнтів не змінюються.",
|
||||
"resetSuccess": "Трафік групи {name} скинуто.",
|
||||
"adjustSuccess": "Скориговано {count} клієнт(ів) у {name}.",
|
||||
"emptyForAction": "У цій групі ще немає клієнтів.",
|
||||
"deleteGroupOnly": "Видалити групу (зберегти клієнтів)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "Đã xóa nhóm khỏi {count} client.",
|
||||
"resetTraffic": "Đặt lại lưu lượng",
|
||||
"resetConfirmTitle": "Đặt lại lưu lượng nhóm {name}?",
|
||||
"resetConfirmContent": "Việc này đưa up/down về 0 cho tất cả {count} client trong nhóm.",
|
||||
"resetSuccess": "Đã đặt lại lưu lượng cho {count} client.",
|
||||
"resetConfirmContent": "Việc này chỉ đặt lại bộ đếm lưu lượng của nhóm. Bộ đếm của từng client không bị ảnh hưởng.",
|
||||
"resetSuccess": "Đã đặt lại lưu lượng nhóm {name}.",
|
||||
"adjustSuccess": "Đã điều chỉnh {count} client trong {name}.",
|
||||
"emptyForAction": "Nhóm này chưa có client.",
|
||||
"deleteGroupOnly": "Xóa nhóm (giữ client)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "已清除 {count} 个客户端的分组。",
|
||||
"resetTraffic": "重置流量",
|
||||
"resetConfirmTitle": "重置分组 {name} 的流量?",
|
||||
"resetConfirmContent": "这将清零此分组中所有 {count} 个客户端的上行/下行流量。",
|
||||
"resetSuccess": "已重置 {count} 个客户端的流量。",
|
||||
"resetConfirmContent": "这只会清零该分组的流量计数器,不影响各个客户端的计数器。",
|
||||
"resetSuccess": "已重置分组 {name} 的流量。",
|
||||
"adjustSuccess": "已调整 {name} 中的 {count} 个客户端。",
|
||||
"emptyForAction": "此分组尚无客户端。",
|
||||
"deleteGroupOnly": "删除分组(保留客户端)",
|
||||
|
|
|
|||
|
|
@ -934,8 +934,8 @@
|
|||
"deleteSuccess": "已清除 {count} 個客戶端的群組。",
|
||||
"resetTraffic": "重置流量",
|
||||
"resetConfirmTitle": "重置群組 {name} 的流量?",
|
||||
"resetConfirmContent": "這將將此群組中所有 {count} 個客戶端的上行/下行流量歸零。",
|
||||
"resetSuccess": "已重置 {count} 個客戶端的流量。",
|
||||
"resetConfirmContent": "這只會將此群組的流量計數器歸零,不影響各個客戶端的計數器。",
|
||||
"resetSuccess": "已重置群組 {name} 的流量。",
|
||||
"adjustSuccess": "已調整 {name} 中的 {count} 個客戶端。",
|
||||
"emptyForAction": "此群組尚無客戶端。",
|
||||
"deleteGroupOnly": "刪除群組(保留客戶端)",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue