订阅和规则组支持从文件导入,配置页与表单一批打磨
文件导入走两步:内容先经新端点 POST /api/provider-files 写进内核工作 目录的 providers/ 下(路径白名单校验,只认两个路径助手生成的形状), 成了再写 file 类型的声明——顺序反了内核重载会去读不存在的文件。 file 类型没有地址和自动更新(内核靠文件监视跟进),详情页照此展示, 编辑按钮禁用。ini 导入的取文件按钮换成同一个拖放模块。 订阅的更新间隔改成开关语义:内核只在 interval > 0 时才起定时拉取 (component/resource/fetcher.go:226-228),关掉开关就是不写这个键。 配置页加「更新所有订阅」,详情的「立刻更新」改叫「更新订阅」。 规则表从零开始写入时自动垫上内网直连——保留地址不直连的话第一条 代理规则一生效,访问局域网的流量就全进了代理;按钮改成「补全内网 直连」,只在有缺口时出现。 所有表单对话框点遮罩关闭;失败横幅右侧加关闭按钮。
This commit is contained in:
+105
-10
@@ -10,6 +10,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { collectInbounds } from '@shared/inbounds.ts'
|
||||
import { pruneGenerated, syncGenerated } from '@shared/inbound-groups.ts'
|
||||
import {
|
||||
addFileSubscription,
|
||||
addSubscription,
|
||||
listSubscriptions,
|
||||
providerNames,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
updateSubscription,
|
||||
} from '@shared/providers.ts'
|
||||
import type { Subscription } from '@shared/providers.ts'
|
||||
import { providerCachePath } from '@shared/inbound-groups.ts'
|
||||
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
|
||||
import { ApiError, api } from '../api'
|
||||
import type { ProviderInfo } from '../api'
|
||||
@@ -27,6 +29,8 @@ import ProviderList from '../components/ProviderList.vue'
|
||||
import SubscriptionDetail from './SubscriptionDetail.vue'
|
||||
import SubscriptionForm from './SubscriptionForm.vue'
|
||||
import IconAdd from '~icons/ri/add-line'
|
||||
import IconRefresh from '~icons/ri/refresh-line'
|
||||
import IconClose from '~icons/ri/close-line'
|
||||
|
||||
const props = defineProps<{ data: ConfigObject; revision: string }>()
|
||||
|
||||
@@ -142,11 +146,33 @@ function openEdit() {
|
||||
formOpen.value = true
|
||||
}
|
||||
|
||||
async function submitForm(draft: { name: string; url: string; interval: number }) {
|
||||
async function submitForm(draft: {
|
||||
name: string
|
||||
url: string
|
||||
interval: number | null
|
||||
file: { name: string; content: string } | null
|
||||
}) {
|
||||
const editing = formEditing.value
|
||||
|
||||
// 从文件导入:先把内容写到订阅的缓存路径上,成了再写声明。
|
||||
// 顺序不能反——声明先落盘的话,内核重载会去读一个还不存在的文件
|
||||
if (draft.file) {
|
||||
try {
|
||||
await api.uploadProviderFile(providerCachePath(draft.name), draft.file.content)
|
||||
} catch (error) {
|
||||
failure.value =
|
||||
error instanceof ApiError
|
||||
? { message: error.message, hint: error.hint }
|
||||
: { message: String(error), hint: '' }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const next = editing
|
||||
? updateSubscription(clone(), editing.name, draft.url, draft.interval)
|
||||
: addSubscription(clone(), draft.name, draft.url)
|
||||
: draft.file
|
||||
? addFileSubscription(clone(), draft.name)
|
||||
: addSubscription(clone(), draft.name, draft.url, draft.interval)
|
||||
await commit(next, editing ? '已保存' : `已新增 ${draft.name}`)
|
||||
// 失败时不能关对话框——关了再打开就是空表单,填的名称、地址、间隔全没了
|
||||
if (failure.value) return
|
||||
@@ -194,6 +220,29 @@ async function refresh() {
|
||||
refreshing.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const refreshingAll = ref(false)
|
||||
|
||||
/** 挨个让内核重新拉取所有订阅。失败的点名,成功的不逐个报。 */
|
||||
async function refreshAll() {
|
||||
if (refreshingAll.value || !kernel.value.online || subscriptions.value.length === 0) return
|
||||
refreshingAll.value = true
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
const names = subscriptions.value.map((s) => s.name)
|
||||
const results = await Promise.allSettled(names.map((n) => api.refreshProvider(n)))
|
||||
await Promise.all([reloadRuntime(), reloadProviders()])
|
||||
const failed = names.filter((_, i) => results[i]?.status === 'rejected')
|
||||
if (failed.length > 0) {
|
||||
failure.value = {
|
||||
message: `${failed.join('、')} 更新失败`,
|
||||
hint: '其余订阅已更新。该操作仅修改内核运行状态,配置文件未被修改。',
|
||||
}
|
||||
} else {
|
||||
done.value = `已更新全部 ${names.length} 个订阅`
|
||||
}
|
||||
refreshingAll.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -210,15 +259,31 @@ async function refresh() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="add" type="button" :disabled="saving" @click="openAdd">
|
||||
<IconAdd />
|
||||
<span>新增</span>
|
||||
</button>
|
||||
<div class="head-acts">
|
||||
<button
|
||||
class="add"
|
||||
type="button"
|
||||
:disabled="refreshingAll || !kernel.online || subscriptions.length === 0"
|
||||
@click="refreshAll"
|
||||
>
|
||||
<IconRefresh />
|
||||
<span>{{ refreshingAll ? '更新中' : '更新所有订阅' }}</span>
|
||||
</button>
|
||||
<button class="add" type="button" :disabled="saving" @click="openAdd">
|
||||
<IconAdd />
|
||||
<span>新增</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="failure" class="failure">
|
||||
<strong>{{ failure.message }}</strong>
|
||||
<span v-if="failure.hint">{{ failure.hint }}</span>
|
||||
<div class="failure-text">
|
||||
<strong>{{ failure.message }}</strong>
|
||||
<span v-if="failure.hint">{{ failure.hint }}</span>
|
||||
</div>
|
||||
<button type="button" class="failure-x" title="关闭提示" @click="failure = null">
|
||||
<IconClose />
|
||||
</button>
|
||||
</div>
|
||||
<p v-else-if="done" class="done">{{ done }}</p>
|
||||
|
||||
@@ -310,10 +375,15 @@ async function refresh() {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.head-acts {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.failure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
align-items: flex-start;
|
||||
gap: var(--s-3);
|
||||
border: 1px solid color-mix(in srgb, var(--c-danger) 35%, var(--c-border));
|
||||
border-radius: var(--r-md);
|
||||
background: var(--c-danger-soft);
|
||||
@@ -322,6 +392,31 @@ async function refresh() {
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.failure-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.failure-x {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
color: var(--c-danger);
|
||||
cursor: pointer;
|
||||
}
|
||||
.failure-x:hover {
|
||||
background: color-mix(in srgb, var(--c-danger) 12%, transparent);
|
||||
}
|
||||
|
||||
.done {
|
||||
color: var(--c-ok);
|
||||
font-size: var(--t-sm);
|
||||
|
||||
Reference in New Issue
Block a user