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:
MHSanaei
2026-05-08 10:09:33 +02:00
parent 2fd2cd0af1
commit 12c10dbd98
14 changed files with 176 additions and 22 deletions

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" = "累計總流量"