mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-12 13:10:05 +00:00
feat: add "Last Online" column to client list and modal (Closes #3402) (#3405)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
* feat: persist client last online and expose API * feat(ui): show client last online in table and info modal * i18n: add “Last Online” across locales * chore: format timestamps as HH:mm:ss
This commit is contained in:
parent
664269d513
commit
4a0914cb1e
21 changed files with 71 additions and 7 deletions
|
@ -134,7 +134,7 @@ class DateUtil {
|
|||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
|
|
|
@ -47,6 +47,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
|||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
||||
{"POST", "/onlines", a.inboundController.onlines},
|
||||
{"POST", "/lastOnline", a.inboundController.lastOnline},
|
||||
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
||||
}
|
||||
|
||||
|
|
|
@ -340,6 +340,11 @@ func (a *InboundController) onlines(c *gin.Context) {
|
|||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||
data, err := a.inboundService.GetClientsLastOnline()
|
||||
jsonObj(c, data, err)
|
||||
}
|
||||
|
||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
|
|
|
@ -33,12 +33,17 @@
|
|||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" >
|
||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
|
|
|
@ -807,6 +807,7 @@
|
|||
defaultKey: '',
|
||||
clientCount: [],
|
||||
onlineClients: [],
|
||||
lastOnlineMap: {},
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
|
@ -835,6 +836,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
await this.getLastOnlineMap();
|
||||
await this.getOnlineUsers();
|
||||
|
||||
this.setInbounds(msg.obj);
|
||||
|
@ -849,6 +851,11 @@
|
|||
}
|
||||
this.onlineClients = msg.obj != null ? msg.obj : [];
|
||||
},
|
||||
async getLastOnlineMap() {
|
||||
const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
|
||||
if (!msg.success || !msg.obj) return;
|
||||
this.lastOnlineMap = msg.obj || {}
|
||||
},
|
||||
async getDefaultSettings() {
|
||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||
if (!msg.success) {
|
||||
|
@ -1493,6 +1500,17 @@
|
|||
isClientOnline(email) {
|
||||
return this.onlineClients.includes(email);
|
||||
},
|
||||
getLastOnline(email) {
|
||||
return this.lastOnlineMap[email] || null
|
||||
},
|
||||
formatLastOnline(email) {
|
||||
const ts = this.getLastOnline(email)
|
||||
if (!ts) return '-'
|
||||
if (this.datepicker === 'gregorian') {
|
||||
return DateUtil.formatMillis(ts)
|
||||
}
|
||||
return DateUtil.convertToJalalian(moment(ts))
|
||||
},
|
||||
isRemovable(dbInboundId) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||
},
|
||||
|
|
|
@ -217,6 +217,12 @@
|
|||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "lastOnline" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.comment">
|
||||
<td>{{ i18n "comment" }}</td>
|
||||
<td>
|
||||
|
|
|
@ -967,6 +967,7 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
// Add user in onlineUsers array on traffic
|
||||
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -2187,6 +2188,20 @@ func (s *InboundService) GetOnlineClients() []string {
|
|||
return p.GetOnlineClients()
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
|
||||
db := database.GetDB()
|
||||
var rows []xray.ClientTraffic
|
||||
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]int64, len(rows))
|
||||
for _, r := range rows {
|
||||
result[r.Email] = r.LastOnline
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "فشل"
|
||||
"comment" = "تعليق"
|
||||
"success" = "تم بنجاح"
|
||||
"lastOnline" = "آخر متصل"
|
||||
"getVersion" = "جيب النسخة"
|
||||
"install" = "تثبيت"
|
||||
"clients" = "عملاء"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Failed"
|
||||
"comment" = "Comment"
|
||||
"success" = "Successfully"
|
||||
"lastOnline" = "Last Online"
|
||||
"getVersion" = "Get Version"
|
||||
"install" = "Install"
|
||||
"clients" = "Clients"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Falló"
|
||||
"comment" = "Comentario"
|
||||
"success" = "Éxito"
|
||||
"lastOnline" = "Última conexión"
|
||||
"getVersion" = "Obtener versión"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "ناموفق"
|
||||
"comment" = "توضیحات"
|
||||
"success" = "موفق"
|
||||
"lastOnline" = "آخرین فعالیت"
|
||||
"getVersion" = "دریافت نسخه"
|
||||
"install" = "نصب"
|
||||
"clients" = "کاربران"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Gagal"
|
||||
"comment" = "Komentar"
|
||||
"success" = "Berhasil"
|
||||
"lastOnline" = "Terakhir online"
|
||||
"getVersion" = "Dapatkan Versi"
|
||||
"install" = "Instal"
|
||||
"clients" = "Klien"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "失敗"
|
||||
"comment" = "コメント"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "最終オンライン"
|
||||
"getVersion" = "バージョン取得"
|
||||
"install" = "インストール"
|
||||
"clients" = "クライアント"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Falhou"
|
||||
"comment" = "Comentário"
|
||||
"success" = "Com Sucesso"
|
||||
"lastOnline" = "Última vez online"
|
||||
"getVersion" = "Obter Versão"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Ошибка"
|
||||
"comment" = "Комментарий"
|
||||
"success" = "Успешно"
|
||||
"lastOnline" = "Был(а) в сети"
|
||||
"getVersion" = "Узнать версию"
|
||||
"install" = "Установка"
|
||||
"clients" = "Клиенты"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Başarısız"
|
||||
"comment" = "Yorum"
|
||||
"success" = "Başarılı"
|
||||
"lastOnline" = "Son çevrimiçi"
|
||||
"getVersion" = "Sürümü Al"
|
||||
"install" = "Yükle"
|
||||
"clients" = "Müşteriler"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Помилка"
|
||||
"comment" = "Коментар"
|
||||
"success" = "Успішно"
|
||||
"lastOnline" = "Був(ла) онлайн"
|
||||
"getVersion" = "Отримати версію"
|
||||
"install" = "Встановити"
|
||||
"clients" = "Клієнти"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Thất bại"
|
||||
"comment" = "Bình luận"
|
||||
"success" = "Thành công"
|
||||
"lastOnline" = "Lần online gần nhất"
|
||||
"getVersion" = "Lấy phiên bản"
|
||||
"install" = "Cài đặt"
|
||||
"clients" = "Các khách hàng"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "失败"
|
||||
"comment" = "评论"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "上次在线"
|
||||
"getVersion" = "获取版本"
|
||||
"install" = "安装"
|
||||
"clients" = "客户端"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "失敗"
|
||||
"comment" = "評論"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "上次上線"
|
||||
"getVersion" = "獲取版本"
|
||||
"install" = "安裝"
|
||||
"clients" = "客戶端"
|
||||
|
|
|
@ -11,4 +11,5 @@ type ClientTraffic struct {
|
|||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
||||
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue