mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-08-28 13:56:18 +00:00
Created / Updated fields for clients (#3384)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
* feat(backend): add created_at/updated_at to clients and maintain on create/update backfill existing clients and set updated_at on mutations * feat(frontend): carry created_at/updated_at in client models and round-trip via JSON * feat(frontend): display Created and Updated columns in client table with proper date formatting * i18n: add pages.inbounds.createdAt/updatedAt across all locales * Update inbound.go Remove duplicate code
This commit is contained in:
parent
d10c312e62
commit
2198397197
18 changed files with 205 additions and 4 deletions
|
@ -104,4 +104,6 @@ type Client struct {
|
|||
SubID string `json:"subId" form:"subId"`
|
||||
Comment string `json:"comment" form:"comment"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
CreatedAt int64 `json:"created_at,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
|
|
@ -1817,7 +1817,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
|
@ -1831,6 +1833,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -1846,6 +1850,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
get _expiryTime() {
|
||||
|
@ -1926,7 +1932,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
|
@ -1940,6 +1948,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -1955,6 +1965,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2065,7 +2077,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.password = password;
|
||||
|
@ -2078,6 +2092,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
|
@ -2092,6 +2108,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2107,6 +2125,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2226,7 +2246,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.method = method;
|
||||
|
@ -2240,6 +2262,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
|
@ -2255,6 +2279,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2271,6 +2297,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -278,4 +278,30 @@
|
|||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="createdAt" slot-scope="text, client, index">
|
||||
<template v-if="client.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.created_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
<template slot="updatedAt" slot-scope="text, client, index">
|
||||
<template v-if="client.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.updated_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
{{end}}
|
||||
|
|
|
@ -760,6 +760,8 @@
|
|||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: '{{ i18n "pages.inbounds.createdAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'createdAt' } },
|
||||
{ title: '{{ i18n "pages.inbounds.updatedAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'updatedAt' } },
|
||||
];
|
||||
|
||||
const innerMobileColumns = [
|
||||
|
|
|
@ -175,6 +175,30 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
|||
return inbound, false, err
|
||||
}
|
||||
|
||||
// Ensure created_at and updated_at on clients in settings
|
||||
if len(clients) > 0 {
|
||||
var settings map[string]any
|
||||
if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil {
|
||||
now := time.Now().Unix() * 1000
|
||||
updatedClients := make([]model.Client, 0, len(clients))
|
||||
for _, c := range clients {
|
||||
if c.CreatedAt == 0 {
|
||||
c.CreatedAt = now
|
||||
}
|
||||
c.UpdatedAt = now
|
||||
updatedClients = append(updatedClients, c)
|
||||
}
|
||||
settings["clients"] = updatedClients
|
||||
if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil {
|
||||
inbound.Settings = string(bs)
|
||||
} else {
|
||||
logger.Debug("Unable to marshal inbound settings with timestamps:", err3)
|
||||
}
|
||||
} else if err2 != nil {
|
||||
logger.Debug("Unable to parse inbound settings for timestamps:", err2)
|
||||
}
|
||||
}
|
||||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
switch inbound.Protocol {
|
||||
|
@ -320,6 +344,53 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||
return inbound, false, err
|
||||
}
|
||||
|
||||
// Ensure created_at and updated_at exist in inbound.Settings clients
|
||||
{
|
||||
var oldSettings map[string]any
|
||||
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||
emailToCreated := map[string]int64{}
|
||||
if oldSettings != nil {
|
||||
if oc, ok := oldSettings["clients"].([]any); ok {
|
||||
for _, it := range oc {
|
||||
if m, ok2 := it.(map[string]any); ok2 {
|
||||
if email, ok3 := m["email"].(string); ok3 {
|
||||
switch v := m["created_at"].(type) {
|
||||
case float64:
|
||||
emailToCreated[email] = int64(v)
|
||||
case int64:
|
||||
emailToCreated[email] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var newSettings map[string]any
|
||||
if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil {
|
||||
now := time.Now().Unix() * 1000
|
||||
if nSlice, ok := newSettings["clients"].([]any); ok {
|
||||
for i := range nSlice {
|
||||
if m, ok2 := nSlice[i].(map[string]any); ok2 {
|
||||
email, _ := m["email"].(string)
|
||||
if _, ok3 := m["created_at"]; !ok3 {
|
||||
if v, ok4 := emailToCreated[email]; ok4 && v > 0 {
|
||||
m["created_at"] = v
|
||||
} else {
|
||||
m["created_at"] = now
|
||||
}
|
||||
}
|
||||
m["updated_at"] = now
|
||||
nSlice[i] = m
|
||||
}
|
||||
}
|
||||
newSettings["clients"] = nSlice
|
||||
if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil {
|
||||
inbound.Settings = string(bs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldInbound.Up = inbound.Up
|
||||
oldInbound.Down = inbound.Down
|
||||
oldInbound.Total = inbound.Total
|
||||
|
@ -422,6 +493,17 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
}
|
||||
|
||||
interfaceClients := settings["clients"].([]any)
|
||||
// Add timestamps for new clients being appended
|
||||
nowTs := time.Now().Unix() * 1000
|
||||
for i := range interfaceClients {
|
||||
if cm, ok := interfaceClients[i].(map[string]any); ok {
|
||||
if _, ok2 := cm["created_at"]; !ok2 {
|
||||
cm["created_at"] = nowTs
|
||||
}
|
||||
cm["updated_at"] = nowTs
|
||||
interfaceClients[i] = cm
|
||||
}
|
||||
}
|
||||
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -672,6 +754,25 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||
return false, err
|
||||
}
|
||||
settingsClients := oldSettings["clients"].([]any)
|
||||
// Preserve created_at and set updated_at for the replacing client
|
||||
var preservedCreated any
|
||||
if clientIndex >= 0 && clientIndex < len(settingsClients) {
|
||||
if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {
|
||||
if v, ok2 := oldMap["created_at"]; ok2 {
|
||||
preservedCreated = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(interfaceClients) > 0 {
|
||||
if newMap, ok := interfaceClients[0].(map[string]any); ok {
|
||||
if preservedCreated == nil {
|
||||
preservedCreated = time.Now().Unix() * 1000
|
||||
}
|
||||
newMap["created_at"] = preservedCreated
|
||||
newMap["updated_at"] = time.Now().Unix() * 1000
|
||||
interfaceClients[0] = newMap
|
||||
}
|
||||
}
|
||||
settingsClients[clientIndex] = interfaceClients[0]
|
||||
oldSettings["clients"] = settingsClients
|
||||
|
||||
|
@ -909,10 +1010,16 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
|
|||
oldExpiryTime := c["expiryTime"].(float64)
|
||||
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
|
||||
c["expiryTime"] = newExpiryTime
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
|
||||
break
|
||||
}
|
||||
}
|
||||
// Backfill created_at and updated_at
|
||||
if _, ok := c["created_at"]; !ok {
|
||||
c["created_at"] = time.Now().Unix() * 1000
|
||||
}
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
settings["clients"] = newClients
|
||||
|
@ -1274,6 +1381,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
|
|||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["tgId"] = tgId
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
|
@ -1360,6 +1468,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
|
|||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["enable"] = !clientOldEnabled
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
|
@ -1423,6 +1532,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
|||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["limitIp"] = count
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
|
@ -1481,6 +1591,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
|||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["expiryTime"] = expiry_time
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
|
@ -1542,6 +1653,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
|
|||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["totalGB"] = totalGB * 1024 * 1024 * 1024
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
|
@ -1962,6 +2074,11 @@ func (s *InboundService) MigrationRequirements() {
|
|||
c["flow"] = ""
|
||||
}
|
||||
}
|
||||
// Backfill created_at and updated_at
|
||||
if _, ok := c["created_at"]; !ok {
|
||||
c["created_at"] = time.Now().Unix() * 1000
|
||||
}
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
settings["clients"] = newClients
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "تفاصيل"
|
||||
"transportConfig" = "نقل"
|
||||
"expireDate" = "المدة"
|
||||
"createdAt" = "تاريخ الإنشاء"
|
||||
"updatedAt" = "تاريخ التحديث"
|
||||
"resetTraffic" = "إعادة ضبط الترافيك"
|
||||
"addInbound" = "أضف إدخال"
|
||||
"generalActions" = "إجراءات عامة"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Details"
|
||||
"transportConfig" = "Transport"
|
||||
"expireDate" = "Duration"
|
||||
"createdAt" = "Created"
|
||||
"updatedAt" = "Updated"
|
||||
"resetTraffic" = "Reset Traffic"
|
||||
"addInbound" = "Add Inbound"
|
||||
"generalActions" = "General Actions"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Detalles"
|
||||
"transportConfig" = "Transporte"
|
||||
"expireDate" = "Fecha de Expiración"
|
||||
"createdAt" = "Creado"
|
||||
"updatedAt" = "Actualizado"
|
||||
"resetTraffic" = "Restablecer Tráfico"
|
||||
"addInbound" = "Agregar Entrada"
|
||||
"generalActions" = "Acciones Generales"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "توضیحات"
|
||||
"transportConfig" = "نحوه اتصال"
|
||||
"expireDate" = "مدت زمان"
|
||||
"createdAt" = "ایجاد"
|
||||
"updatedAt" = "بهروزرسانی"
|
||||
"resetTraffic" = "ریست ترافیک"
|
||||
"addInbound" = "افزودن ورودی"
|
||||
"generalActions" = "عملیات کلی"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Rincian"
|
||||
"transportConfig" = "Transport"
|
||||
"expireDate" = "Durasi"
|
||||
"createdAt" = "Dibuat"
|
||||
"updatedAt" = "Diperbarui"
|
||||
"resetTraffic" = "Reset Traffic"
|
||||
"addInbound" = "Tambahkan Masuk"
|
||||
"generalActions" = "Tindakan Umum"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "詳細情報"
|
||||
"transportConfig" = "トランスポート設定"
|
||||
"expireDate" = "有効期限"
|
||||
"createdAt" = "作成"
|
||||
"updatedAt" = "更新"
|
||||
"resetTraffic" = "トラフィックリセット"
|
||||
"addInbound" = "インバウンド追加"
|
||||
"generalActions" = "一般操作"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Detalhes"
|
||||
"transportConfig" = "Transporte"
|
||||
"expireDate" = "Duração"
|
||||
"createdAt" = "Criado"
|
||||
"updatedAt" = "Atualizado"
|
||||
"resetTraffic" = "Redefinir Tráfego"
|
||||
"addInbound" = "Adicionar Inbound"
|
||||
"generalActions" = "Ações Gerais"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Подробнее"
|
||||
"transportConfig" = "Транспорт"
|
||||
"expireDate" = "Дата окончания"
|
||||
"createdAt" = "Создано"
|
||||
"updatedAt" = "Обновлено"
|
||||
"resetTraffic" = "Сброс трафика"
|
||||
"addInbound" = "Создать инбаунд"
|
||||
"generalActions" = "Общие действия"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Detaylar"
|
||||
"transportConfig" = "Taşıma"
|
||||
"expireDate" = "Süre"
|
||||
"createdAt" = "Oluşturuldu"
|
||||
"updatedAt" = "Güncellendi"
|
||||
"resetTraffic" = "Trafiği Sıfırla"
|
||||
"addInbound" = "Gelen Ekle"
|
||||
"generalActions" = "Genel Eylemler"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Деталі"
|
||||
"transportConfig" = "Транспорт"
|
||||
"expireDate" = "Тривалість"
|
||||
"createdAt" = "Створено"
|
||||
"updatedAt" = "Оновлено"
|
||||
"resetTraffic" = "Скинути трафік"
|
||||
"addInbound" = "Додати вхідний"
|
||||
"generalActions" = "Загальні дії"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "Chi tiết"
|
||||
"transportConfig" = "Giao vận"
|
||||
"expireDate" = "Ngày hết hạn"
|
||||
"createdAt" = "Tạo lúc"
|
||||
"updatedAt" = "Cập nhật"
|
||||
"resetTraffic" = "Đặt lại lưu lượng"
|
||||
"addInbound" = "Thêm điểm vào"
|
||||
"generalActions" = "Hành động chung"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "详细信息"
|
||||
"transportConfig" = "传输配置"
|
||||
"expireDate" = "到期时间"
|
||||
"createdAt" = "创建时间"
|
||||
"updatedAt" = "更新时间"
|
||||
"resetTraffic" = "重置流量"
|
||||
"addInbound" = "添加入站"
|
||||
"generalActions" = "通用操作"
|
||||
|
|
|
@ -165,6 +165,8 @@
|
|||
"details" = "詳細資訊"
|
||||
"transportConfig" = "傳輸配置"
|
||||
"expireDate" = "到期時間"
|
||||
"createdAt" = "建立時間"
|
||||
"updatedAt" = "更新時間"
|
||||
"resetTraffic" = "重置流量"
|
||||
"addInbound" = "新增入站"
|
||||
"generalActions" = "通用操作"
|
||||
|
|
Loading…
Reference in a new issue