mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-23 19:37:13 +00:00
feat(groups): show upload/download breakdown in group traffic
Add per-group up/down to GroupSummary (backend + schema), surface them as Upload/Download columns in the groups table, and fold upload/download into the Total traffic summary card. Rename the group "Clients in group" column to just "Clients" across all locales.
This commit is contained in:
@@ -22,6 +22,8 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { MenuProps, TableColumnsType } from 'antd';
|
import type { MenuProps, TableColumnsType } from 'antd';
|
||||||
import {
|
import {
|
||||||
|
ArrowDownOutlined,
|
||||||
|
ArrowUpOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -165,6 +167,14 @@ export default function GroupsPage() {
|
|||||||
() => groups.reduce((acc, g) => acc + (g.trafficUsed || 0), 0),
|
() => groups.reduce((acc, g) => acc + (g.trafficUsed || 0), 0),
|
||||||
[groups],
|
[groups],
|
||||||
);
|
);
|
||||||
|
const totalUpload = useMemo(
|
||||||
|
() => groups.reduce((acc, g) => acc + (g.up || 0), 0),
|
||||||
|
[groups],
|
||||||
|
);
|
||||||
|
const totalDownload = useMemo(
|
||||||
|
() => groups.reduce((acc, g) => acc + (g.down || 0), 0),
|
||||||
|
[groups],
|
||||||
|
);
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
setCreateName('');
|
setCreateName('');
|
||||||
@@ -417,6 +427,20 @@ export default function GroupsPage() {
|
|||||||
width: 180,
|
width: 180,
|
||||||
render: (count: number) => <span>{count || 0}</span>,
|
render: (count: number) => <span>{count || 0}</span>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('pages.groups.upload'),
|
||||||
|
dataIndex: 'up',
|
||||||
|
key: 'up',
|
||||||
|
width: 140,
|
||||||
|
render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pages.groups.download'),
|
||||||
|
dataIndex: 'down',
|
||||||
|
key: 'down',
|
||||||
|
width: 140,
|
||||||
|
render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('pages.groups.trafficUsed'),
|
title: t('pages.groups.trafficUsed'),
|
||||||
dataIndex: 'trafficUsed',
|
dataIndex: 'trafficUsed',
|
||||||
@@ -456,26 +480,30 @@ export default function GroupsPage() {
|
|||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Card size="small" hoverable className="summary-card">
|
<Card size="small" hoverable className="summary-card">
|
||||||
<Row gutter={[16, isMobile ? 16 : 12]}>
|
<Row gutter={[16, isMobile ? 16 : 12]}>
|
||||||
<Col xs={12} sm={8} md={6}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.groups.totalGroups')}
|
title={t('pages.groups.totalGroups')}
|
||||||
value={String(totalGroups)}
|
value={String(totalGroups)}
|
||||||
prefix={<TagsOutlined />}
|
prefix={<TagsOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} md={6}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.groups.totalGroupedClients')}
|
title={t('pages.groups.totalGroupedClients')}
|
||||||
value={String(totalClients)}
|
value={String(totalClients)}
|
||||||
prefix={<TeamOutlined />}
|
prefix={<TeamOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} md={6}>
|
<Col xs={24} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.groups.totalTraffic')}
|
title={t('pages.groups.totalTraffic')}
|
||||||
value={SizeFormatter.sizeFormat(totalTraffic)}
|
value={SizeFormatter.sizeFormat(totalTraffic)}
|
||||||
prefix={<RetweetOutlined />}
|
prefix={<RetweetOutlined />}
|
||||||
/>
|
/>
|
||||||
|
<Space size={16} style={{ marginTop: 4, color: 'var(--ant-color-text-secondary)', fontSize: 13 }}>
|
||||||
|
<span><ArrowUpOutlined /> {SizeFormatter.sizeFormat(totalUpload)}</span>
|
||||||
|
<span><ArrowDownOutlined /> {SizeFormatter.sizeFormat(totalDownload)}</span>
|
||||||
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ export const GroupSummarySchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
clientCount: z.number(),
|
clientCount: z.number(),
|
||||||
trafficUsed: z.number().nullable().transform((v) => v ?? 0),
|
trafficUsed: z.number().nullable().transform((v) => v ?? 0),
|
||||||
|
up: z.number().nullable().transform((v) => v ?? 0),
|
||||||
|
down: z.number().nullable().transform((v) => v ?? 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []);
|
export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ type GroupSummary struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ClientCount int `json:"clientCount"`
|
ClientCount int `json:"clientCount"`
|
||||||
TrafficUsed int64 `json:"trafficUsed"`
|
TrafficUsed int64 `json:"trafficUsed"`
|
||||||
|
Up int64 `json:"up"`
|
||||||
|
Down int64 `json:"down"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
||||||
@@ -22,7 +24,7 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
|||||||
// never double-counts a client's traffic.
|
// never double-counts a client's traffic.
|
||||||
var derived []GroupSummary
|
var derived []GroupSummary
|
||||||
if err := db.Table("clients AS c").
|
if err := db.Table("clients AS c").
|
||||||
Select("c.group_name AS name, COUNT(*) AS client_count, COALESCE(SUM(ct.up + ct.down), 0) AS traffic_used").
|
Select("c.group_name AS name, COUNT(*) AS client_count, COALESCE(SUM(ct.up + ct.down), 0) AS traffic_used, 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").
|
Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
|
||||||
Where("c.group_name <> ''").
|
Where("c.group_name <> ''").
|
||||||
Group("c.group_name").
|
Group("c.group_name").
|
||||||
@@ -36,17 +38,19 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
|||||||
type groupAgg struct {
|
type groupAgg struct {
|
||||||
count int
|
count int
|
||||||
traffic int64
|
traffic int64
|
||||||
|
up int64
|
||||||
|
down int64
|
||||||
}
|
}
|
||||||
merged := make(map[string]groupAgg, len(derived)+len(stored))
|
merged := make(map[string]groupAgg, len(derived)+len(stored))
|
||||||
for _, g := range stored {
|
for _, g := range stored {
|
||||||
merged[g.Name] = groupAgg{}
|
merged[g.Name] = groupAgg{}
|
||||||
}
|
}
|
||||||
for _, g := range derived {
|
for _, g := range derived {
|
||||||
merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed}
|
merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed, up: g.Up, down: g.Down}
|
||||||
}
|
}
|
||||||
out := make([]GroupSummary, 0, len(merged))
|
out := make([]GroupSummary, 0, len(merged))
|
||||||
for name, agg := range merged {
|
for name, agg := range merged {
|
||||||
out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic})
|
out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic, Up: agg.up, Down: agg.down})
|
||||||
}
|
}
|
||||||
sort.Slice(out, func(i, j int) bool {
|
sort.Slice(out, func(i, j int) bool {
|
||||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "المجموعات",
|
"title": "المجموعات",
|
||||||
"name": "الاسم",
|
"name": "الاسم",
|
||||||
"clientCount": "عملاء في المجموعة",
|
"clientCount": "العملاء",
|
||||||
"totalGroups": "إجمالي المجموعات",
|
"totalGroups": "إجمالي المجموعات",
|
||||||
"totalGroupedClients": "العملاء بمجموعة",
|
"totalGroupedClients": "العملاء بمجموعة",
|
||||||
"trafficUsed": "حركة المرور المستخدمة",
|
"trafficUsed": "حركة المرور المستخدمة",
|
||||||
|
"upload": "رفع",
|
||||||
|
"download": "تنزيل",
|
||||||
"totalTraffic": "إجمالي حركة المرور",
|
"totalTraffic": "إجمالي حركة المرور",
|
||||||
"addGroup": "إضافة مجموعة",
|
"addGroup": "إضافة مجموعة",
|
||||||
"createSuccess": "تم إنشاء المجموعة «{name}».",
|
"createSuccess": "تم إنشاء المجموعة «{name}».",
|
||||||
|
|||||||
@@ -813,10 +813,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Groups",
|
"title": "Groups",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"clientCount": "Clients in group",
|
"clientCount": "Clients",
|
||||||
"totalGroups": "Total groups",
|
"totalGroups": "Total groups",
|
||||||
"totalGroupedClients": "Clients with a group",
|
"totalGroupedClients": "Clients with a group",
|
||||||
"trafficUsed": "Traffic used",
|
"trafficUsed": "Traffic used",
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
"totalTraffic": "Total traffic",
|
"totalTraffic": "Total traffic",
|
||||||
"addGroup": "Add Group",
|
"addGroup": "Add Group",
|
||||||
"createSuccess": "Group \"{name}\" created.",
|
"createSuccess": "Group \"{name}\" created.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Grupos",
|
"title": "Grupos",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"clientCount": "Clientes en el grupo",
|
"clientCount": "Clientes",
|
||||||
"totalGroups": "Total de grupos",
|
"totalGroups": "Total de grupos",
|
||||||
"totalGroupedClients": "Clientes con grupo",
|
"totalGroupedClients": "Clientes con grupo",
|
||||||
"trafficUsed": "Tráfico usado",
|
"trafficUsed": "Tráfico usado",
|
||||||
|
"upload": "Subida",
|
||||||
|
"download": "Bajada",
|
||||||
"totalTraffic": "Tráfico total",
|
"totalTraffic": "Tráfico total",
|
||||||
"addGroup": "Añadir grupo",
|
"addGroup": "Añadir grupo",
|
||||||
"createSuccess": "Grupo «{name}» creado.",
|
"createSuccess": "Grupo «{name}» creado.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "گروهها",
|
"title": "گروهها",
|
||||||
"name": "نام",
|
"name": "نام",
|
||||||
"clientCount": "کاربران در گروه",
|
"clientCount": "کاربران",
|
||||||
"totalGroups": "تعداد گروهها",
|
"totalGroups": "تعداد گروهها",
|
||||||
"totalGroupedClients": "کاربران دارای گروه",
|
"totalGroupedClients": "کاربران دارای گروه",
|
||||||
"trafficUsed": "ترافیک مصرفشده",
|
"trafficUsed": "ترافیک مصرفشده",
|
||||||
|
"upload": "آپلود",
|
||||||
|
"download": "دانلود",
|
||||||
"totalTraffic": "مجموع ترافیک",
|
"totalTraffic": "مجموع ترافیک",
|
||||||
"addGroup": "افزودن گروه",
|
"addGroup": "افزودن گروه",
|
||||||
"createSuccess": "گروه «{name}» ایجاد شد.",
|
"createSuccess": "گروه «{name}» ایجاد شد.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Grup",
|
"title": "Grup",
|
||||||
"name": "Nama",
|
"name": "Nama",
|
||||||
"clientCount": "Klien di grup",
|
"clientCount": "Klien",
|
||||||
"totalGroups": "Total grup",
|
"totalGroups": "Total grup",
|
||||||
"totalGroupedClients": "Klien dengan grup",
|
"totalGroupedClients": "Klien dengan grup",
|
||||||
"trafficUsed": "Trafik terpakai",
|
"trafficUsed": "Trafik terpakai",
|
||||||
|
"upload": "Unggah",
|
||||||
|
"download": "Unduh",
|
||||||
"totalTraffic": "Total trafik",
|
"totalTraffic": "Total trafik",
|
||||||
"addGroup": "Tambah grup",
|
"addGroup": "Tambah grup",
|
||||||
"createSuccess": "Grup «{name}» dibuat.",
|
"createSuccess": "Grup «{name}» dibuat.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "グループ",
|
"title": "グループ",
|
||||||
"name": "名前",
|
"name": "名前",
|
||||||
"clientCount": "グループ内のクライアント",
|
"clientCount": "クライアント",
|
||||||
"totalGroups": "グループ合計",
|
"totalGroups": "グループ合計",
|
||||||
"totalGroupedClients": "グループのあるクライアント",
|
"totalGroupedClients": "グループのあるクライアント",
|
||||||
"trafficUsed": "使用済みトラフィック",
|
"trafficUsed": "使用済みトラフィック",
|
||||||
|
"upload": "アップロード",
|
||||||
|
"download": "ダウンロード",
|
||||||
"totalTraffic": "合計トラフィック",
|
"totalTraffic": "合計トラフィック",
|
||||||
"addGroup": "グループ追加",
|
"addGroup": "グループ追加",
|
||||||
"createSuccess": "グループ「{name}」を作成しました。",
|
"createSuccess": "グループ「{name}」を作成しました。",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Grupos",
|
"title": "Grupos",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"clientCount": "Clientes no grupo",
|
"clientCount": "Clientes",
|
||||||
"totalGroups": "Total de grupos",
|
"totalGroups": "Total de grupos",
|
||||||
"totalGroupedClients": "Clientes com grupo",
|
"totalGroupedClients": "Clientes com grupo",
|
||||||
"trafficUsed": "Tráfego usado",
|
"trafficUsed": "Tráfego usado",
|
||||||
|
"upload": "Envio",
|
||||||
|
"download": "Recebimento",
|
||||||
"totalTraffic": "Tráfego total",
|
"totalTraffic": "Tráfego total",
|
||||||
"addGroup": "Adicionar grupo",
|
"addGroup": "Adicionar grupo",
|
||||||
"createSuccess": "Grupo «{name}» criado.",
|
"createSuccess": "Grupo «{name}» criado.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Группы",
|
"title": "Группы",
|
||||||
"name": "Имя",
|
"name": "Имя",
|
||||||
"clientCount": "Клиентов в группе",
|
"clientCount": "Клиенты",
|
||||||
"totalGroups": "Всего групп",
|
"totalGroups": "Всего групп",
|
||||||
"totalGroupedClients": "Клиенты с группой",
|
"totalGroupedClients": "Клиенты с группой",
|
||||||
"trafficUsed": "Использованный трафик",
|
"trafficUsed": "Использованный трафик",
|
||||||
|
"upload": "Отправлено",
|
||||||
|
"download": "Получено",
|
||||||
"totalTraffic": "Общий трафик",
|
"totalTraffic": "Общий трафик",
|
||||||
"addGroup": "Добавить группу",
|
"addGroup": "Добавить группу",
|
||||||
"createSuccess": "Группа «{name}» создана.",
|
"createSuccess": "Группа «{name}» создана.",
|
||||||
|
|||||||
@@ -813,10 +813,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Gruplar",
|
"title": "Gruplar",
|
||||||
"name": "İsim",
|
"name": "İsim",
|
||||||
"clientCount": "Gruptaki kullanıcılar",
|
"clientCount": "Kullanıcılar",
|
||||||
"totalGroups": "Toplam grup",
|
"totalGroups": "Toplam grup",
|
||||||
"totalGroupedClients": "Grubu olan kullanıcılar",
|
"totalGroupedClients": "Grubu olan kullanıcılar",
|
||||||
"trafficUsed": "Kullanılan trafik",
|
"trafficUsed": "Kullanılan trafik",
|
||||||
|
"upload": "Yükleme",
|
||||||
|
"download": "İndirme",
|
||||||
"totalTraffic": "Toplam trafik",
|
"totalTraffic": "Toplam trafik",
|
||||||
"addGroup": "Grup ekle",
|
"addGroup": "Grup ekle",
|
||||||
"createSuccess": "«{name}» grubu oluşturuldu.",
|
"createSuccess": "«{name}» grubu oluşturuldu.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Групи",
|
"title": "Групи",
|
||||||
"name": "Назва",
|
"name": "Назва",
|
||||||
"clientCount": "Клієнтів у групі",
|
"clientCount": "Клієнти",
|
||||||
"totalGroups": "Всього груп",
|
"totalGroups": "Всього груп",
|
||||||
"totalGroupedClients": "Клієнти з групою",
|
"totalGroupedClients": "Клієнти з групою",
|
||||||
"trafficUsed": "Використаний трафік",
|
"trafficUsed": "Використаний трафік",
|
||||||
|
"upload": "Вивантаження",
|
||||||
|
"download": "Завантаження",
|
||||||
"totalTraffic": "Загальний трафік",
|
"totalTraffic": "Загальний трафік",
|
||||||
"addGroup": "Додати групу",
|
"addGroup": "Додати групу",
|
||||||
"createSuccess": "Групу «{name}» створено.",
|
"createSuccess": "Групу «{name}» створено.",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "Nhóm",
|
"title": "Nhóm",
|
||||||
"name": "Tên",
|
"name": "Tên",
|
||||||
"clientCount": "Client trong nhóm",
|
"clientCount": "Client",
|
||||||
"totalGroups": "Tổng số nhóm",
|
"totalGroups": "Tổng số nhóm",
|
||||||
"totalGroupedClients": "Client có nhóm",
|
"totalGroupedClients": "Client có nhóm",
|
||||||
"trafficUsed": "Lưu lượng đã dùng",
|
"trafficUsed": "Lưu lượng đã dùng",
|
||||||
|
"upload": "Tải lên",
|
||||||
|
"download": "Tải xuống",
|
||||||
"totalTraffic": "Tổng lưu lượng",
|
"totalTraffic": "Tổng lưu lượng",
|
||||||
"addGroup": "Thêm nhóm",
|
"addGroup": "Thêm nhóm",
|
||||||
"createSuccess": "Đã tạo nhóm «{name}».",
|
"createSuccess": "Đã tạo nhóm «{name}».",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "分组",
|
"title": "分组",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
"clientCount": "分组中的客户端",
|
"clientCount": "客户端",
|
||||||
"totalGroups": "分组总数",
|
"totalGroups": "分组总数",
|
||||||
"totalGroupedClients": "有分组的客户端",
|
"totalGroupedClients": "有分组的客户端",
|
||||||
"trafficUsed": "已用流量",
|
"trafficUsed": "已用流量",
|
||||||
|
"upload": "上传",
|
||||||
|
"download": "下载",
|
||||||
"totalTraffic": "总流量",
|
"totalTraffic": "总流量",
|
||||||
"addGroup": "添加分组",
|
"addGroup": "添加分组",
|
||||||
"createSuccess": "已创建分组 “{name}”。",
|
"createSuccess": "已创建分组 “{name}”。",
|
||||||
|
|||||||
@@ -812,10 +812,12 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"title": "群組",
|
"title": "群組",
|
||||||
"name": "名稱",
|
"name": "名稱",
|
||||||
"clientCount": "群組中的客戶端",
|
"clientCount": "客戶端",
|
||||||
"totalGroups": "群組總數",
|
"totalGroups": "群組總數",
|
||||||
"totalGroupedClients": "有群組的客戶端",
|
"totalGroupedClients": "有群組的客戶端",
|
||||||
"trafficUsed": "已用流量",
|
"trafficUsed": "已用流量",
|
||||||
|
"upload": "上傳",
|
||||||
|
"download": "下載",
|
||||||
"totalTraffic": "總流量",
|
"totalTraffic": "總流量",
|
||||||
"addGroup": "新增群組",
|
"addGroup": "新增群組",
|
||||||
"createSuccess": "已建立群組「{name}」。",
|
"createSuccess": "已建立群組「{name}」。",
|
||||||
|
|||||||
Reference in New Issue
Block a user