订阅和规则组支持从文件导入,配置页与表单一批打磨

文件导入走两步:内容先经新端点 POST /api/provider-files 写进内核工作
目录的 providers/ 下(路径白名单校验,只认两个路径助手生成的形状),
成了再写 file 类型的声明——顺序反了内核重载会去读不存在的文件。
file 类型没有地址和自动更新(内核靠文件监视跟进),详情页照此展示,
编辑按钮禁用。ini 导入的取文件按钮换成同一个拖放模块。

订阅的更新间隔改成开关语义:内核只在 interval > 0 时才起定时拉取
(component/resource/fetcher.go:226-228),关掉开关就是不写这个键。
配置页加「更新所有订阅」,详情的「立刻更新」改叫「更新订阅」。

规则表从零开始写入时自动垫上内网直连——保留地址不直连的话第一条
代理规则一生效,访问局域网的流量就全进了代理;按钮改成「补全内网
直连」,只在有缺口时出现。

所有表单对话框点遮罩关闭;失败横幅右侧加关闭按钮。
This commit is contained in:
2026-08-12 00:46:17 +08:00
parent 7d08f37248
commit 013f537eaa
19 changed files with 871 additions and 101 deletions
+69 -9
View File
@@ -39,6 +39,7 @@ import IconEdit from '~icons/ri/edit-line'
import IconDelete from '~icons/ri/delete-bin-line'
import IconRefresh from '~icons/ri/refresh-line'
import IconLoad from '~icons/ri/loader-4-line'
import IconClose from '~icons/ri/close-line'
const props = defineProps<{ data: ConfigObject; revision: string }>()
@@ -139,11 +140,21 @@ function openEditRule(index: number) {
async function submitRule(ruleText: string) {
const editing = ruleEditing.value
// 规则表从零开始时先垫上内网直连——保留地址不直连的话,第一条代理规则一生效,
// 访问路由器、局域网服务的流量就全进了代理。ini 导入不垫:配方自带直连规则
const seed = editing === null && rules.value.length === 0 ? missingLan.value : []
const next =
editing === null
? insertBeforeMatch(rules.value, [ruleText])
? insertBeforeMatch([...seed, ...rules.value], [ruleText])
: rules.value.map((rule, i) => (i === editing ? ruleText : rule))
await commit(withRules(next), editing === null ? '已新增规则' : '已保存')
await commit(
withRules(next),
editing === null
? seed.length > 0
? `已新增规则,并附带 ${seed.length} 条内网直连`
: '已新增规则'
: '已保存',
)
// 失败时不能关对话框——关了再打开就是空表单
if (!failure.value) ruleFormOpen.value = false
}
@@ -208,7 +219,24 @@ function formatTime(value: string | undefined): string {
const groupFormOpen = ref(false)
const importOpen = ref(false)
async function submitGroup(entry: { name: string; declaration: ConfigObject }) {
async function submitGroup(entry: {
name: string
declaration: ConfigObject
file: { content: string } | null
}) {
// 上传的清单先落盘再写声明。顺序不能反——声明先生效的话,内核重载会去读
// 一个还不存在的文件
if (entry.file && typeof entry.declaration.path === 'string') {
try {
await api.uploadProviderFile(entry.declaration.path, entry.file.content)
} catch (error) {
failure.value = {
message: error instanceof ApiError ? error.message : String(error),
hint: '清单没有写入,配置文件也没有改动。',
}
return
}
}
const next = clone()
setRuleProviders(next, { ...ruleProvidersOf(next), [entry.name]: entry.declaration })
await commit(next, `已新增规则组 ${entry.name}`)
@@ -308,8 +336,13 @@ async function updateNow(name: string) {
</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>
@@ -318,12 +351,14 @@ async function updateNow(name: string) {
<h2>分流规则</h2>
<div class="sec-acts">
<button
v-if="missingLan.length > 0 && rules.length > 0"
type="button"
class="btn"
:disabled="saving || missingLan.length === 0"
:disabled="saving"
:title="`补上缺的 ${missingLan.length} 条保留地址直连规则`"
@click="applyLanPreset"
>
内网直连
补全内网直连
</button>
<button type="button" class="btn" :disabled="saving" @click="openAddRule">
<IconAdd />
@@ -546,8 +581,8 @@ async function updateNow(name: string) {
.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);
@@ -556,6 +591,31 @@ async function updateNow(name: string) {
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);