mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-08 14:36:13 +00:00
feat(custom-geo): refresh index UI
Split the single ext-snippet column into Alias / URL / Routing /
Last-updated, with the alias surfaced next to a colored type tag,
the URL ellipsized with a tooltip + open-in-new-tab, and the
ext:file.dat:tag snippet click-to-copy via ClipboardManager.
Switch Last-updated to a relative time ("2 hours ago") with the
absolute timestamp on hover, add a friendly empty state, and show
a result toast when "Update All" finishes with partial failures.
customGeoEmpty translated for all 13 locales.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"customGeoErrNotFound" = "مصدر geo المخصص غير موجود"
|
||||
"customGeoErrDownload" = "فشل التنزيل"
|
||||
"customGeoErrUpdateAllIncomplete" = "تعذر تحديث مصدر واحد أو أكثر من مصادر geo المخصصة"
|
||||
"customGeoEmpty" = "لا توجد مصادر geo مخصصة بعد — انقر على «إضافة» لإنشاء واحد"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "إجمالي حركة المرور"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"customGeoErrNotFound" = "منبع geo سفارشی یافت نشد"
|
||||
"customGeoErrDownload" = "بارگیری ناموفق بود"
|
||||
"customGeoErrUpdateAllIncomplete" = "بهروزرسانی یک یا چند منبع geo سفارشی ناموفق بود"
|
||||
"customGeoEmpty" = "هنوز منبع geo سفارشیای ثبت نشده — برای ایجاد روی «افزودن» کلیک کنید"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "کل ترافیک"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"customGeoErrNotFound" = "カスタム geo ソースが見つかりません"
|
||||
"customGeoErrDownload" = "ダウンロードに失敗しました"
|
||||
"customGeoErrUpdateAllIncomplete" = "カスタム geo ソースの 1 件以上を更新できませんでした"
|
||||
"customGeoEmpty" = "カスタム geo ソースはまだありません — 「追加」をクリックして作成してください"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "総トラフィック"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"customGeoErrNotFound" = "Источник не найден"
|
||||
"customGeoErrDownload" = "Ошибка загрузки"
|
||||
"customGeoErrUpdateAllIncomplete" = "Не удалось обновить один или несколько пользовательских источников"
|
||||
"customGeoEmpty" = "Пользовательских источников geo пока нет — нажмите «Добавить», чтобы создать"
|
||||
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
|
||||
"logs" = "Журнал"
|
||||
"config" = "Конфигурация"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"customGeoErrNotFound" = "Джерело geo не знайдено"
|
||||
"customGeoErrDownload" = "Помилка завантаження"
|
||||
"customGeoErrUpdateAllIncomplete" = "Не вдалося оновити один або кілька користувацьких джерел"
|
||||
"customGeoEmpty" = "Користувацьких джерел geo поки немає — натисніть «Додати», щоб створити"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Загальний трафік"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"customGeoErrNotFound" = "未找到自定义 geo 源"
|
||||
"customGeoErrDownload" = "下载失败"
|
||||
"customGeoErrUpdateAllIncomplete" = "有一个或多个自定义 geo 源更新失败"
|
||||
"customGeoEmpty" = "暂无自定义 geo 源 — 点击「添加」以创建"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "累计总流量"
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"customGeoErrNotFound" = "找不到自訂 geo 來源"
|
||||
"customGeoErrDownload" = "下載失敗"
|
||||
"customGeoErrUpdateAllIncomplete" = "有一個或多個自訂 geo 來源更新失敗"
|
||||
"customGeoEmpty" = "尚無自訂 geo 來源 — 點擊「新增」以建立"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "累計總流量"
|
||||
|
||||
Reference in New Issue
Block a user