Merge branch 'main' into feat/daily-traffic-speed

This commit is contained in:
stivfilippov 2026-05-08 14:02:36 +03:00 committed by GitHub
commit e3662f482f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 191 additions and 26 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,8 @@ import (
"strconv"
"time"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/web/entity"
"github.com/mhsanaei/3x-ui/v2/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/websocket"
@ -135,10 +137,17 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
}
// getPanelUpdateInfo retrieves the current and latest panel version.
// Network failures (e.g. no internet, GitHub blocked) are logged at debug
// level only — the panel keeps working offline and we don't want to spam
// WARN every time a user opens the page.
func (a *ServerController) getPanelUpdateInfo(c *gin.Context) {
info, err := a.panelService.GetUpdateInfo()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.index.panelUpdateCheckPopover"), err)
logger.Debug("panel update check failed:", err)
c.JSON(http.StatusOK, entity.Msg{
Success: false,
Msg: I18nWeb(c, "pages.index.panelUpdateCheckPopover"),
})
return
}
jsonObj(c, info, nil)

View file

@ -3,12 +3,98 @@
{{ template "page/body_start" .}}
<style>
.custom-geo-section code.custom-geo-ext-code {
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
background: rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.08);
}
.custom-geo-copyable {
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.custom-geo-copyable:hover {
background: rgba(24, 144, 255, 0.12);
border-color: rgba(24, 144, 255, 0.45);
}
.custom-geo-alias-cell {
display: flex;
align-items: center;
gap: 6px;
}
.custom-geo-alias {
font-weight: 500;
}
.custom-geo-type-tag {
margin-right: 0;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.4px;
}
.custom-geo-url {
display: inline-block;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.custom-geo-muted {
color: rgba(0, 0, 0, 0.35);
}
.custom-geo-count {
background: rgba(0, 0, 0, 0.06);
color: rgba(0, 0, 0, 0.55);
border-radius: 10px;
padding: 1px 8px;
font-size: 12px;
}
.custom-geo-empty {
padding: 24px 0;
color: rgba(0, 0, 0, 0.45);
text-align: center;
}
.custom-geo-empty-icon {
font-size: 32px;
color: rgba(0, 0, 0, 0.25);
display: block;
margin: 0 auto 8px;
}
body.dark .custom-geo-section code.custom-geo-ext-code {
color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.85));
background: var(--dark-color-surface-200, #222d42);
border: 1px solid var(--dark-color-stroke, #2c3950);
padding: 2px 6px;
border-radius: 3px;
}
body.dark .custom-geo-copyable:hover {
background: rgba(24, 144, 255, 0.18);
border-color: rgba(64, 169, 255, 0.55);
}
body.dark .custom-geo-muted,
body.dark .custom-geo-empty {
color: rgba(255, 255, 255, 0.45);
}
body.dark .custom-geo-empty-icon {
color: rgba(255, 255, 255, 0.25);
}
body.dark .custom-geo-count {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.7);
}
html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code {
@ -383,21 +469,43 @@
<div class="custom-geo-section">
<a-alert type="info" show-icon class="mb-10"
message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
<div class="mb-10">
<div class="mb-10 d-flex align-center" style="flex-wrap: wrap; gap: 8px;">
<a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
{{ i18n "pages.index.customGeoAdd" }}
</a-button>
<a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n
"pages.index.geofilesUpdateAll" }}</a-button>
<a-button icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll"
:disabled="!customGeoList.length">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button>
<span v-if="customGeoList.length" class="custom-geo-count">[[ customGeoList.length ]]</span>
</div>
<a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
:loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
:loading="customGeoLoading" size="small" :scroll="{ x: 760 }">
<template slot="alias" slot-scope="text, record">
<div class="custom-geo-alias-cell">
<a-tag :color="record.type === 'geoip' ? 'cyan' : 'purple'"
class="custom-geo-type-tag">[[ record.type ]]</a-tag>
<span class="custom-geo-alias">[[ record.alias ]]</span>
</div>
</template>
<template slot="url" slot-scope="text, record">
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme" placement="topLeft">
<template slot="title">[[ record.url ]]</template>
<a :href="record.url" target="_blank" rel="noopener noreferrer"
class="custom-geo-url">[[ record.url ]]</a>
</a-tooltip>
</template>
<template slot="extDat" slot-scope="text, record">
<code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "copy" }}</template>
<code class="custom-geo-ext-code custom-geo-copyable"
@click="copyCustomGeoExt(record)">[[ customGeoExtDisplay(record) ]]</code>
</a-tooltip>
</template>
<template slot="lastUpdatedAt" slot-scope="text, record">
<span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
<span v-else></span>
<a-tooltip v-if="record.lastUpdatedAt" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</template>
<span>[[ customGeoRelativeTime(record.lastUpdatedAt) ]]</span>
</a-tooltip>
<span v-else class="custom-geo-muted"></span>
</template>
<template slot="action" slot-scope="text, record">
<a-space size="small">
@ -416,6 +524,12 @@
</a-tooltip>
</a-space>
</template>
<template slot="emptyText">
<div class="custom-geo-empty">
<a-icon type="inbox" class="custom-geo-empty-icon"></a-icon>
<div>{{ i18n "pages.index.customGeoEmpty" }}</div>
</div>
</template>
</a-table>
</div>
</a-collapse-panel>
@ -1111,29 +1225,34 @@
};
const customGeoColumns = [{
title: '{{ i18n "pages.index.customGeoAlias" }}',
key: 'alias',
scopedSlots: { customRender: 'alias' },
width: 200
},
{
title: '{{ i18n "pages.index.customGeoUrl" }}',
key: 'url',
scopedSlots: { customRender: 'url' },
ellipsis: true
},
{
title: '{{ i18n "pages.index.customGeoExtColumn" }}',
key: 'extDat',
scopedSlots: {
customRender: 'extDat'
},
ellipsis: true
scopedSlots: { customRender: 'extDat' },
width: 220
},
{
title: '{{ i18n "pages.index.customGeoLastUpdated" }}',
key: 'lastUpdatedAt',
scopedSlots: {
customRender: 'lastUpdatedAt'
},
width: 160
scopedSlots: { customRender: 'lastUpdatedAt' },
width: 140
},
{
title: '{{ i18n "pages.index.customGeoActions" }}',
key: 'action',
scopedSlots: {
customRender: 'action'
},
scopedSlots: { customRender: 'action' },
width: 120,
fixed: 'right'
},
];
@ -1266,12 +1385,29 @@
if (!ts) return '';
return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts);
},
customGeoRelativeTime(ts) {
if (!ts) return '';
if (typeof moment === 'undefined') return String(ts);
return moment(ts * 1000).fromNow();
},
customGeoExtDisplay(record) {
const fn = record.type === 'geoip' ?
`geoip_${record.alias}.dat` :
`geosite_${record.alias}.dat`;
return `ext:${fn}:tag`;
},
copyCustomGeoExt(record) {
const text = this.customGeoExtDisplay(record);
if (typeof ClipboardManager !== 'undefined' && ClipboardManager.copyText) {
ClipboardManager.copyText(text).then(ok => {
if (ok) this.$message.success(`{{ i18n "copy" }}: ${text}`);
});
} else if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success(`{{ i18n "copy" }}: ${text}`);
});
}
},
async loadCustomGeo() {
this.customGeoLoading = true;
try {
@ -1376,8 +1512,13 @@
this.customGeoUpdatingAll = true;
try {
const msg = await HttpUtil.post('/panel/api/custom-geo/update-all');
if (msg.success || (msg.obj && Array.isArray(msg.obj.succeeded) && msg.obj.succeeded.length > 0)) {
const ok = (msg && msg.obj && Array.isArray(msg.obj.succeeded)) ? msg.obj.succeeded.length : 0;
const failed = (msg && msg.obj && Array.isArray(msg.obj.failed)) ? msg.obj.failed.length : 0;
if (msg.success || ok > 0) {
await this.loadCustomGeo();
if (failed > 0) {
this.$message.warning(`Updated ${ok}, failed ${failed}`);
}
}
} finally {
this.customGeoUpdatingAll = false;

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "مصدر geo المخصص غير موجود"
"customGeoErrDownload" = "فشل التنزيل"
"customGeoErrUpdateAllIncomplete" = "تعذر تحديث مصدر واحد أو أكثر من مصادر geo المخصصة"
"customGeoEmpty" = "لا توجد مصادر geo مخصصة بعد — انقر على «إضافة» لإنشاء واحد"
[pages.inbounds]
"allTimeTraffic" = "إجمالي حركة المرور"

View file

@ -204,6 +204,7 @@
"customGeoErrNotFound" = "Custom geo source not found"
"customGeoErrDownload" = "Download failed"
"customGeoErrUpdateAllIncomplete" = "One or more custom geo sources failed to update"
"customGeoEmpty" = "No custom geo sources yet — click Add to create one"
"dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs"
"config" = "Config"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Fuente geo personalizada no encontrada"
"customGeoErrDownload" = "Error de descarga"
"customGeoErrUpdateAllIncomplete" = "No se pudieron actualizar una o más fuentes geo personalizadas"
"customGeoEmpty" = "Aún no hay fuentes geo personalizadas — haz clic en Añadir para crear una"
[pages.inbounds]
"allTimeTraffic" = "Tráfico Total"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "منبع geo سفارشی یافت نشد"
"customGeoErrDownload" = "بارگیری ناموفق بود"
"customGeoErrUpdateAllIncomplete" = "به‌روزرسانی یک یا چند منبع geo سفارشی ناموفق بود"
"customGeoEmpty" = "هنوز منبع geo سفارشی‌ای ثبت نشده — برای ایجاد روی «افزودن» کلیک کنید"
[pages.inbounds]
"allTimeTraffic" = "کل ترافیک"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Sumber geo kustom tidak ditemukan"
"customGeoErrDownload" = "Unduh gagal"
"customGeoErrUpdateAllIncomplete" = "Satu atau lebih sumber geo kustom gagal diperbarui"
"customGeoEmpty" = "Belum ada sumber geo kustom — klik Tambah untuk membuatnya"
[pages.inbounds]
"allTimeTraffic" = "Total Lalu Lintas"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "カスタム geo ソースが見つかりません"
"customGeoErrDownload" = "ダウンロードに失敗しました"
"customGeoErrUpdateAllIncomplete" = "カスタム geo ソースの 1 件以上を更新できませんでした"
"customGeoEmpty" = "カスタム geo ソースはまだありません — 「追加」をクリックして作成してください"
[pages.inbounds]
"allTimeTraffic" = "総トラフィック"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Fonte geo personalizada não encontrada"
"customGeoErrDownload" = "Falha no download"
"customGeoErrUpdateAllIncomplete" = "Falha ao atualizar uma ou mais fontes geo personalizadas"
"customGeoEmpty" = "Ainda não há fontes geo personalizadas — clique em Adicionar para criar uma"
[pages.inbounds]
"allTimeTraffic" = "Tráfego Total"

View file

@ -204,6 +204,7 @@
"customGeoErrNotFound" = "Источник не найден"
"customGeoErrDownload" = "Ошибка загрузки"
"customGeoErrUpdateAllIncomplete" = "Не удалось обновить один или несколько пользовательских источников"
"customGeoEmpty" = "Пользовательских источников geo пока нет — нажмите «Добавить», чтобы создать"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал"
"config" = "Конфигурация"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Özel geo kaynağı bulunamadı"
"customGeoErrDownload" = "İndirme başarısız"
"customGeoErrUpdateAllIncomplete" = "Bir veya daha fazla özel geo kaynağı güncellenemedi"
"customGeoEmpty" = "Henüz özel geo kaynağı yok — oluşturmak için Ekle'ye tıklayın"
[pages.inbounds]
"allTimeTraffic" = "Toplam Trafik"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Джерело geo не знайдено"
"customGeoErrDownload" = "Помилка завантаження"
"customGeoErrUpdateAllIncomplete" = "Не вдалося оновити один або кілька користувацьких джерел"
"customGeoEmpty" = "Користувацьких джерел geo поки немає — натисніть «Додати», щоб створити"
[pages.inbounds]
"allTimeTraffic" = "Загальний трафік"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Không tìm thấy nguồn geo tùy chỉnh"
"customGeoErrDownload" = "Tải xuống thất bại"
"customGeoErrUpdateAllIncomplete" = "Một hoặc nhiều nguồn geo tùy chỉnh không cập nhật được"
"customGeoEmpty" = "Chưa có nguồn geo tùy chỉnh nào — nhấp Thêm để tạo"
[pages.inbounds]
"allTimeTraffic" = "Tổng Lưu Lượng"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "未找到自定义 geo 源"
"customGeoErrDownload" = "下载失败"
"customGeoErrUpdateAllIncomplete" = "有一个或多个自定义 geo 源更新失败"
"customGeoEmpty" = "暂无自定义 geo 源 — 点击「添加」以创建"
[pages.inbounds]
"allTimeTraffic" = "累计总流量"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "找不到自訂 geo 來源"
"customGeoErrDownload" = "下載失敗"
"customGeoErrUpdateAllIncomplete" = "有一個或多個自訂 geo 來源更新失敗"
"customGeoEmpty" = "尚無自訂 geo 來源 — 點擊「新增」以建立"
[pages.inbounds]
"allTimeTraffic" = "累計總流量"