mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-12 21:20:07 +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) {
|
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() {
|
static firstDayOfMonth() {
|
||||||
|
|
|
@ -47,6 +47,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
||||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
||||||
{"POST", "/onlines", a.inboundController.onlines},
|
{"POST", "/onlines", a.inboundController.onlines},
|
||||||
|
{"POST", "/lastOnline", a.inboundController.lastOnline},
|
||||||
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -340,6 +340,11 @@ func (a *InboundController) onlines(c *gin.Context) {
|
||||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
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) {
|
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
|
|
|
@ -33,12 +33,17 @@
|
||||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
<template slot="online" slot-scope="text, client, index">
|
<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)">
|
<template v-if="client.enable && isClientOnline(client.email)">
|
||||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
<template slot="client" slot-scope="text, client">
|
<template slot="client" slot-scope="text, client">
|
||||||
<a-space direction="horizontal" :size="2">
|
<a-space direction="horizontal" :size="2">
|
||||||
|
|
|
@ -807,6 +807,7 @@
|
||||||
defaultKey: '',
|
defaultKey: '',
|
||||||
clientCount: [],
|
clientCount: [],
|
||||||
onlineClients: [],
|
onlineClients: [],
|
||||||
|
lastOnlineMap: {},
|
||||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
|
@ -835,6 +836,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.getLastOnlineMap();
|
||||||
await this.getOnlineUsers();
|
await this.getOnlineUsers();
|
||||||
|
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
|
@ -849,6 +851,11 @@
|
||||||
}
|
}
|
||||||
this.onlineClients = msg.obj != null ? msg.obj : [];
|
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() {
|
async getDefaultSettings() {
|
||||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
|
@ -1493,6 +1500,17 @@
|
||||||
isClientOnline(email) {
|
isClientOnline(email) {
|
||||||
return this.onlineClients.includes(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) {
|
isRemovable(dbInboundId) {
|
||||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||||
},
|
},
|
||||||
|
|
|
@ -217,6 +217,12 @@
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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">
|
<tr v-if="infoModal.clientSettings.comment">
|
||||||
<td>{{ i18n "comment" }}</td>
|
<td>{{ i18n "comment" }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -967,6 +967,7 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
||||||
// Add user in onlineUsers array on traffic
|
// Add user in onlineUsers array on traffic
|
||||||
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
||||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||||
|
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -2187,6 +2188,20 @@ func (s *InboundService) GetOnlineClients() []string {
|
||||||
return p.GetOnlineClients()
|
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) {
|
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "فشل"
|
"fail" = "فشل"
|
||||||
"comment" = "تعليق"
|
"comment" = "تعليق"
|
||||||
"success" = "تم بنجاح"
|
"success" = "تم بنجاح"
|
||||||
|
"lastOnline" = "آخر متصل"
|
||||||
"getVersion" = "جيب النسخة"
|
"getVersion" = "جيب النسخة"
|
||||||
"install" = "تثبيت"
|
"install" = "تثبيت"
|
||||||
"clients" = "عملاء"
|
"clients" = "عملاء"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Failed"
|
"fail" = "Failed"
|
||||||
"comment" = "Comment"
|
"comment" = "Comment"
|
||||||
"success" = "Successfully"
|
"success" = "Successfully"
|
||||||
|
"lastOnline" = "Last Online"
|
||||||
"getVersion" = "Get Version"
|
"getVersion" = "Get Version"
|
||||||
"install" = "Install"
|
"install" = "Install"
|
||||||
"clients" = "Clients"
|
"clients" = "Clients"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Falló"
|
"fail" = "Falló"
|
||||||
"comment" = "Comentario"
|
"comment" = "Comentario"
|
||||||
"success" = "Éxito"
|
"success" = "Éxito"
|
||||||
|
"lastOnline" = "Última conexión"
|
||||||
"getVersion" = "Obtener versión"
|
"getVersion" = "Obtener versión"
|
||||||
"install" = "Instalar"
|
"install" = "Instalar"
|
||||||
"clients" = "Clientes"
|
"clients" = "Clientes"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "ناموفق"
|
"fail" = "ناموفق"
|
||||||
"comment" = "توضیحات"
|
"comment" = "توضیحات"
|
||||||
"success" = "موفق"
|
"success" = "موفق"
|
||||||
|
"lastOnline" = "آخرین فعالیت"
|
||||||
"getVersion" = "دریافت نسخه"
|
"getVersion" = "دریافت نسخه"
|
||||||
"install" = "نصب"
|
"install" = "نصب"
|
||||||
"clients" = "کاربران"
|
"clients" = "کاربران"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Gagal"
|
"fail" = "Gagal"
|
||||||
"comment" = "Komentar"
|
"comment" = "Komentar"
|
||||||
"success" = "Berhasil"
|
"success" = "Berhasil"
|
||||||
|
"lastOnline" = "Terakhir online"
|
||||||
"getVersion" = "Dapatkan Versi"
|
"getVersion" = "Dapatkan Versi"
|
||||||
"install" = "Instal"
|
"install" = "Instal"
|
||||||
"clients" = "Klien"
|
"clients" = "Klien"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "失敗"
|
"fail" = "失敗"
|
||||||
"comment" = "コメント"
|
"comment" = "コメント"
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
|
"lastOnline" = "最終オンライン"
|
||||||
"getVersion" = "バージョン取得"
|
"getVersion" = "バージョン取得"
|
||||||
"install" = "インストール"
|
"install" = "インストール"
|
||||||
"clients" = "クライアント"
|
"clients" = "クライアント"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Falhou"
|
"fail" = "Falhou"
|
||||||
"comment" = "Comentário"
|
"comment" = "Comentário"
|
||||||
"success" = "Com Sucesso"
|
"success" = "Com Sucesso"
|
||||||
|
"lastOnline" = "Última vez online"
|
||||||
"getVersion" = "Obter Versão"
|
"getVersion" = "Obter Versão"
|
||||||
"install" = "Instalar"
|
"install" = "Instalar"
|
||||||
"clients" = "Clientes"
|
"clients" = "Clientes"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Ошибка"
|
"fail" = "Ошибка"
|
||||||
"comment" = "Комментарий"
|
"comment" = "Комментарий"
|
||||||
"success" = "Успешно"
|
"success" = "Успешно"
|
||||||
|
"lastOnline" = "Был(а) в сети"
|
||||||
"getVersion" = "Узнать версию"
|
"getVersion" = "Узнать версию"
|
||||||
"install" = "Установка"
|
"install" = "Установка"
|
||||||
"clients" = "Клиенты"
|
"clients" = "Клиенты"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Başarısız"
|
"fail" = "Başarısız"
|
||||||
"comment" = "Yorum"
|
"comment" = "Yorum"
|
||||||
"success" = "Başarılı"
|
"success" = "Başarılı"
|
||||||
|
"lastOnline" = "Son çevrimiçi"
|
||||||
"getVersion" = "Sürümü Al"
|
"getVersion" = "Sürümü Al"
|
||||||
"install" = "Yükle"
|
"install" = "Yükle"
|
||||||
"clients" = "Müşteriler"
|
"clients" = "Müşteriler"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Помилка"
|
"fail" = "Помилка"
|
||||||
"comment" = "Коментар"
|
"comment" = "Коментар"
|
||||||
"success" = "Успішно"
|
"success" = "Успішно"
|
||||||
|
"lastOnline" = "Був(ла) онлайн"
|
||||||
"getVersion" = "Отримати версію"
|
"getVersion" = "Отримати версію"
|
||||||
"install" = "Встановити"
|
"install" = "Встановити"
|
||||||
"clients" = "Клієнти"
|
"clients" = "Клієнти"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "Thất bại"
|
"fail" = "Thất bại"
|
||||||
"comment" = "Bình luận"
|
"comment" = "Bình luận"
|
||||||
"success" = "Thành công"
|
"success" = "Thành công"
|
||||||
|
"lastOnline" = "Lần online gần nhất"
|
||||||
"getVersion" = "Lấy phiên bản"
|
"getVersion" = "Lấy phiên bản"
|
||||||
"install" = "Cài đặt"
|
"install" = "Cài đặt"
|
||||||
"clients" = "Các khách hàng"
|
"clients" = "Các khách hàng"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "失败"
|
"fail" = "失败"
|
||||||
"comment" = "评论"
|
"comment" = "评论"
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
|
"lastOnline" = "上次在线"
|
||||||
"getVersion" = "获取版本"
|
"getVersion" = "获取版本"
|
||||||
"install" = "安装"
|
"install" = "安装"
|
||||||
"clients" = "客户端"
|
"clients" = "客户端"
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"fail" = "失敗"
|
"fail" = "失敗"
|
||||||
"comment" = "評論"
|
"comment" = "評論"
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
|
"lastOnline" = "上次上線"
|
||||||
"getVersion" = "獲取版本"
|
"getVersion" = "獲取版本"
|
||||||
"install" = "安裝"
|
"install" = "安裝"
|
||||||
"clients" = "客戶端"
|
"clients" = "客戶端"
|
||||||
|
|
|
@ -11,4 +11,5 @@ type ClientTraffic struct {
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||||
Total int64 `json:"total" form:"total"`
|
Total int64 `json:"total" form:"total"`
|
||||||
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
||||||
|
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue