Add custom geosite/geoip URL sources (#3980)
* feat: add custom geosite/geoip URL sources Register DB model, panel API, index/xray UI, and i18n. * fix
This commit is contained in:
committed by
GitHub
parent
96b568b838
commit
7466916e02
+220
-2
@@ -2,6 +2,20 @@
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<style>
|
||||
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;
|
||||
}
|
||||
html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code {
|
||||
color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.88));
|
||||
background: var(--dark-color-surface-700, #111929);
|
||||
border-color: var(--dark-color-stroke, #2c3950);
|
||||
}
|
||||
</style>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' index-page'">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
@@ -105,7 +119,7 @@
|
||||
</a-row>
|
||||
</span>
|
||||
<template slot="content">
|
||||
<span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
||||
<span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line ]]</span>
|
||||
</template>
|
||||
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"
|
||||
:class="status.xray.color === 'red' ? 'xray-error-animation' : ''" />
|
||||
@@ -113,7 +127,7 @@
|
||||
</template>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" class="jc-center">
|
||||
<a-space v-if="ipLimitEnable" direction="horizontal" @click="openXrayLogs()" class="jc-center">
|
||||
<a-icon type="bars"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
||||
</a-space>
|
||||
@@ -328,8 +342,65 @@
|
||||
<div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n
|
||||
"pages.index.geofilesUpdateAll" }}</a-button></div>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="3" header='{{ i18n "pages.index.customGeoTitle" }}'>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
|
||||
:loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
|
||||
<template slot="extDat" slot-scope="text, record">
|
||||
<code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
|
||||
</template>
|
||||
<template slot="lastUpdatedAt" slot-scope="text, record">
|
||||
<span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
<template slot="action" slot-scope="text, record">
|
||||
<a-space size="small">
|
||||
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template>
|
||||
<a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template>
|
||||
<a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)" :loading="customGeoActionId === record.id"></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template>
|
||||
<a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-modal>
|
||||
<a-modal v-model="customGeoModal.visible" :title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'"
|
||||
:confirm-loading="customGeoModal.saving" @ok="submitCustomGeo" :ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label='{{ i18n "pages.index.customGeoType" }}'>
|
||||
<a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="geosite">geosite</a-select-option>
|
||||
<a-select-option value="geoip">geoip</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.index.customGeoAlias" }}'>
|
||||
<a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId" placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.index.customGeoUrl" }}'>
|
||||
<a-input v-model.trim="customGeoModal.form.url" placeholder="https://"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<a-modal id="log-modal" v-model="logModal.visible" :closable="true" @cancel="() => logModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme" width="800px" footer="">
|
||||
<template slot="title">
|
||||
@@ -870,6 +941,12 @@
|
||||
},
|
||||
};
|
||||
|
||||
const customGeoColumns = [
|
||||
{ title: '{{ i18n "pages.index.customGeoExtColumn" }}', key: 'extDat', scopedSlots: { customRender: 'extDat' }, ellipsis: true },
|
||||
{ title: '{{ i18n "pages.index.customGeoLastUpdated" }}', key: 'lastUpdatedAt', scopedSlots: { customRender: 'lastUpdatedAt' }, width: 160 },
|
||||
{ title: '{{ i18n "pages.index.customGeoActions" }}', key: 'action', scopedSlots: { customRender: 'action' }, width: 120, fixed: 'right' },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
@@ -893,6 +970,25 @@
|
||||
showAlert: false,
|
||||
showIp: false,
|
||||
ipLimitEnable: false,
|
||||
customGeoColumns,
|
||||
customGeoList: [],
|
||||
customGeoLoading: false,
|
||||
customGeoUpdatingAll: false,
|
||||
customGeoActionId: null,
|
||||
customGeoModal: {
|
||||
visible: false,
|
||||
editId: null,
|
||||
saving: false,
|
||||
form: {
|
||||
type: 'geosite',
|
||||
alias: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
customGeoValidation: {
|
||||
alias: '{{ i18n "pages.index.customGeoValidationAlias" }}',
|
||||
url: '{{ i18n "pages.index.customGeoValidationUrl" }}',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loading(spinning, tip = '{{ i18n "loading"}}') {
|
||||
@@ -961,6 +1057,128 @@
|
||||
return;
|
||||
}
|
||||
versionModal.show(msg.obj);
|
||||
this.loadCustomGeo();
|
||||
},
|
||||
customGeoFormatTime(ts) {
|
||||
if (!ts) return '';
|
||||
return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts);
|
||||
},
|
||||
customGeoExtDisplay(record) {
|
||||
const fn = record.type === 'geoip'
|
||||
? `geoip_${record.alias}.dat`
|
||||
: `geosite_${record.alias}.dat`;
|
||||
return `ext:${fn}:tag`;
|
||||
},
|
||||
async loadCustomGeo() {
|
||||
this.customGeoLoading = true;
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/api/custom-geo/list');
|
||||
if (msg.success && Array.isArray(msg.obj)) {
|
||||
this.customGeoList = msg.obj;
|
||||
}
|
||||
} finally {
|
||||
this.customGeoLoading = false;
|
||||
}
|
||||
},
|
||||
openCustomGeoModal(record) {
|
||||
if (record) {
|
||||
this.customGeoModal.editId = record.id;
|
||||
this.customGeoModal.form = {
|
||||
type: record.type,
|
||||
alias: record.alias,
|
||||
url: record.url,
|
||||
};
|
||||
} else {
|
||||
this.customGeoModal.editId = null;
|
||||
this.customGeoModal.form = {
|
||||
type: 'geosite',
|
||||
alias: '',
|
||||
url: '',
|
||||
};
|
||||
}
|
||||
this.customGeoModal.visible = true;
|
||||
},
|
||||
validateCustomGeoForm() {
|
||||
const f = this.customGeoModal.form;
|
||||
const re = /^[a-z0-9_-]+$/;
|
||||
if (!re.test(f.alias || '')) {
|
||||
this.$message.error(this.customGeoValidation.alias);
|
||||
return false;
|
||||
}
|
||||
const u = (f.url || '').trim();
|
||||
if (!/^https?:\/\//i.test(u)) {
|
||||
this.$message.error(this.customGeoValidation.url);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(u);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
this.$message.error(this.customGeoValidation.url);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.customGeoValidation.url);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async submitCustomGeo() {
|
||||
if (!this.validateCustomGeoForm()) {
|
||||
return;
|
||||
}
|
||||
const f = this.customGeoModal.form;
|
||||
this.customGeoModal.saving = true;
|
||||
try {
|
||||
let msg;
|
||||
if (this.customGeoModal.editId) {
|
||||
msg = await HttpUtil.post(`/panel/api/custom-geo/update/${this.customGeoModal.editId}`, f);
|
||||
} else {
|
||||
msg = await HttpUtil.post('/panel/api/custom-geo/add', f);
|
||||
}
|
||||
if (msg && msg.success) {
|
||||
this.customGeoModal.visible = false;
|
||||
await this.loadCustomGeo();
|
||||
}
|
||||
} finally {
|
||||
this.customGeoModal.saving = false;
|
||||
}
|
||||
},
|
||||
confirmDeleteCustomGeo(record) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.index.customGeoDelete" }}',
|
||||
content: '{{ i18n "pages.index.customGeoDeleteConfirm" }}',
|
||||
okText: '{{ i18n "confirm"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
onOk: async () => {
|
||||
const msg = await HttpUtil.post(`/panel/api/custom-geo/delete/${record.id}`);
|
||||
if (msg.success) {
|
||||
await this.loadCustomGeo();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
async downloadCustomGeo(id) {
|
||||
this.customGeoActionId = id;
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/custom-geo/download/${id}`);
|
||||
if (msg.success) {
|
||||
await this.loadCustomGeo();
|
||||
}
|
||||
} finally {
|
||||
this.customGeoActionId = null;
|
||||
}
|
||||
},
|
||||
async updateAllCustomGeo() {
|
||||
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)) {
|
||||
await this.loadCustomGeo();
|
||||
}
|
||||
} finally {
|
||||
this.customGeoUpdatingAll = false;
|
||||
}
|
||||
},
|
||||
switchV2rayVersion(version) {
|
||||
this.$confirm({
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
refreshing: false,
|
||||
restartResult: '',
|
||||
showAlert: false,
|
||||
customGeoAliasLabelSuffix: '{{ i18n "pages.index.customGeoAliasLabelSuffix" }}',
|
||||
advSettings: 'xraySetting',
|
||||
obsSettings: '',
|
||||
cm: null,
|
||||
@@ -1054,6 +1055,31 @@
|
||||
},
|
||||
showWarp() {
|
||||
warpModal.show();
|
||||
},
|
||||
async loadCustomGeoAliases() {
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/api/custom-geo/aliases');
|
||||
if (!msg.success) {
|
||||
console.warn('Failed to load custom geo aliases:', msg.msg || 'request failed');
|
||||
return;
|
||||
}
|
||||
if (!msg.obj) return;
|
||||
const { geoip = [], geosite = [] } = msg.obj;
|
||||
const geoSuffix = this.customGeoAliasLabelSuffix || '';
|
||||
geoip.forEach((x) => {
|
||||
this.settingsData.IPsOptions.push({
|
||||
label: x.alias + geoSuffix,
|
||||
value: x.extExample,
|
||||
});
|
||||
});
|
||||
geosite.forEach((x) => {
|
||||
const opt = { label: x.alias + geoSuffix, value: x.extExample };
|
||||
this.settingsData.DomainsOptions.push(opt);
|
||||
this.settingsData.BlockDomainsOptions.push(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load custom geo aliases:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -1061,6 +1087,7 @@
|
||||
this.showAlert = true;
|
||||
}
|
||||
await this.getXraySetting();
|
||||
await this.loadCustomGeoAliases();
|
||||
await this.getXrayResult();
|
||||
await this.getOutboundsTraffic();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user