feat: GeoIP-based geo detection + configurable rename template
- Add MaxMind GeoLite2-City mmdb auto-download and IP-based geo lookup (covers all 249 countries globally, replacing limited 8-country regex) - Add RenameOptions with 5 toggleable fields (flag/alias/country/city/index) stored as rename_options_json on collections - Collection rename pipeline uses DetectGeoWithServer(name, server) which tries GeoIP lookup first, falls back to name-based regex matching - Chinese country/city names + emoji flag prefix - Country-city dedup: skip city when it equals country (e.g. 香港/香港) - URI fragment encoding: url.QueryEscape -> url.PathEscape (%20 not +) - Egress cache key excludes name field so renamed nodes still match cache - Frontend: 5 toggle buttons in collection edit form, default all on - Migrations: 0004 (source alias + collection rename_enabled), 0005 (collection rename_options_json)
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
<span>原始节点: {{ nodeCounts[col.id]?.original ?? '-' }}</span>
|
||||
<span>处理后: {{ nodeCounts[col.id]?.processed ?? '-' }}</span>
|
||||
<span>订阅源: {{ col.sourceIds?.length || 0 }}</span>
|
||||
<span>过滤器: {{ col.filters?.length || 0 }}</span>
|
||||
<span v-if="col.renameEnabled" class="text-purple-500">重命名已开启</span>
|
||||
<span>模板: {{ col.templateId || 'default' }}</span>
|
||||
<span v-if="col.ignoreFailed">忽略失败</span>
|
||||
</div>
|
||||
@@ -114,18 +114,23 @@
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" v-model="form.ignoreFailed" class="rounded" /> 忽略失败源
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" v-model="form.renameEnabled" class="rounded" /> 节点重命名
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-sm font-medium">过滤器</label>
|
||||
<button class="text-xs text-primary-600 hover:underline" @click="form.filters.push({ type: 'include', pattern: '' })">+ 添加</button>
|
||||
</div>
|
||||
<div v-for="(f, i) in form.filters" :key="i" class="flex gap-1 mb-1.5">
|
||||
<select v-model="f.type" class="px-2 py-1 border rounded text-xs w-28">
|
||||
<option v-for="t in filterTypes" :key="t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<input v-model="f.pattern" placeholder="pattern" class="flex-1 px-2 py-1 border rounded text-xs" />
|
||||
<button class="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded" @click="form.filters.splice(i,1)">×</button>
|
||||
<div v-if="form.renameEnabled" class="rounded-lg bg-blue-50 px-3 py-2 text-xs text-blue-600">
|
||||
开启后,所有节点将按以下格式重命名:<br>
|
||||
<code class="text-blue-800">[国旗] [别名] [国家] [城市] [序号]</code><br>
|
||||
点击下方按钮可切换各字段的显示(默认全开):
|
||||
<div class="flex flex-wrap gap-1.5 mt-2">
|
||||
<button type="button" v-for="opt in renameOptionList" :key="opt.key"
|
||||
class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
|
||||
:class="form.renameOptions[opt.key]
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-500 border-gray-300 hover:border-gray-400'"
|
||||
@click="form.renameOptions[opt.key] = !form.renameOptions[opt.key]">
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,8 +174,18 @@ const confirmDialog = ref({ show: false, title: '', message: '', danger: false,
|
||||
|
||||
const filterTypes = ['include', 'exclude', 'rename', 'dedupe', 'sort', 'delete-field', 'flag', 'quick', 'resolve', 'custom']
|
||||
|
||||
const renameOptionList = [
|
||||
{ key: 'flag', label: '国旗' },
|
||||
{ key: 'alias', label: '别名' },
|
||||
{ key: 'country', label: '国家' },
|
||||
{ key: 'city', label: '城市' },
|
||||
{ key: 'index', label: '序号' },
|
||||
]
|
||||
|
||||
const defaultRenameOptions = () => ({ flag: true, alias: true, country: true, city: true, index: true })
|
||||
|
||||
const emptyForm = () => ({
|
||||
name: '', sourceIds: [], filters: [], templateId: '', ignoreFailed: true, enabled: true
|
||||
name: '', sourceIds: [], renameEnabled: false, renameOptions: defaultRenameOptions(), templateId: '', ignoreFailed: true, enabled: true
|
||||
})
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
@@ -217,10 +232,18 @@ function openCreate() {
|
||||
}
|
||||
|
||||
function editCol(col) {
|
||||
const ro = col.renameOptions || {}
|
||||
Object.assign(form, {
|
||||
name: col.id,
|
||||
sourceIds: [...(col.sourceIds || [])],
|
||||
filters: JSON.parse(JSON.stringify(col.filters || [])),
|
||||
renameEnabled: col.renameEnabled === true,
|
||||
renameOptions: {
|
||||
flag: ro.flag !== false,
|
||||
alias: ro.alias !== false,
|
||||
country: ro.country !== false,
|
||||
city: ro.city !== false,
|
||||
index: ro.index !== false,
|
||||
},
|
||||
templateId: col.templateId || '',
|
||||
ignoreFailed: col.ignoreFailed !== false,
|
||||
enabled: col.enabled !== false,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-900">{{ src.name }}</span>
|
||||
<span v-if="src.alias" class="px-1.5 py-0.5 rounded text-xs bg-purple-100 text-purple-700">{{ src.alias }}</span>
|
||||
<span class="px-1.5 py-0.5 rounded text-xs"
|
||||
:class="src.type === 'remote' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'">{{ src.type }}</span>
|
||||
<span v-if="!src.enabled" class="px-1.5 py-0.5 rounded text-xs bg-red-100 text-red-700">已禁用</span>
|
||||
@@ -22,6 +23,7 @@
|
||||
<div class="flex gap-2 mt-1 text-xs text-gray-400">
|
||||
<span>原始节点: {{ nodeCounts[src.id]?.original ?? '-' }}</span>
|
||||
<span>处理后: {{ nodeCounts[src.id]?.processed ?? '-' }}</span>
|
||||
<span v-if="src.alias">别名: {{ src.alias }}</span>
|
||||
<span>过滤器: {{ src.filters?.length || 0 }}</span>
|
||||
<span v-if="src.createdAt">创建: {{ formatTime(src.createdAt) }}</span>
|
||||
</div>
|
||||
@@ -87,11 +89,19 @@
|
||||
<!-- Create/Edit Modal -->
|
||||
<Modal v-model="showModal" :title="editing ? '编辑订阅源' : '新建订阅源'">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">名称 / ID</label>
|
||||
<input v-model="form.name" :disabled="editing" placeholder="my-sub"
|
||||
class="w-full px-3 py-2 border rounded-lg text-sm disabled:bg-gray-100" />
|
||||
<p class="text-xs text-gray-400 mt-0.5">仅支持小写字母、数字、下划线、连字符</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">名称 / ID</label>
|
||||
<input v-model="form.name" :disabled="editing" placeholder="my-sub"
|
||||
class="w-full px-3 py-2 border rounded-lg text-sm disabled:bg-gray-100" />
|
||||
<p class="text-xs text-gray-400 mt-0.5">小写字母、数字、下划线、连字符</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">别名 (Alias)</label>
|
||||
<input v-model="form.alias" placeholder="如:机场A"
|
||||
class="w-full px-3 py-2 border rounded-lg text-sm" />
|
||||
<p class="text-xs text-gray-400 mt-0.5">合集重命名时作为节点标识</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">类型</label>
|
||||
@@ -225,7 +235,7 @@ const filterGuides = [
|
||||
]
|
||||
|
||||
const emptyForm = () => ({
|
||||
name: '', type: 'remote', url: '', content: '', enabled: true, filters: []
|
||||
name: '', alias: '', type: 'remote', url: '', content: '', enabled: true, filters: []
|
||||
})
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
@@ -305,6 +315,7 @@ function sourcePreviewPayload(src) {
|
||||
return {
|
||||
id: src.id,
|
||||
name: src.name,
|
||||
alias: src.alias,
|
||||
type: src.type,
|
||||
url: src.url,
|
||||
content: src.content,
|
||||
@@ -321,6 +332,7 @@ function openCreate() {
|
||||
function editSource(src) {
|
||||
Object.assign(form, {
|
||||
name: src.id,
|
||||
alias: src.alias || '',
|
||||
type: src.type || 'remote',
|
||||
url: src.url || '',
|
||||
content: src.content || '',
|
||||
@@ -339,6 +351,7 @@ async function save() {
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name,
|
||||
alias: form.alias,
|
||||
type: form.type,
|
||||
url: form.url,
|
||||
content: form.content,
|
||||
|
||||
Reference in New Issue
Block a user