订阅和规则组支持从文件导入,配置页与表单一批打磨
文件导入走两步:内容先经新端点 POST /api/provider-files 写进内核工作 目录的 providers/ 下(路径白名单校验,只认两个路径助手生成的形状), 成了再写 file 类型的声明——顺序反了内核重载会去读不存在的文件。 file 类型没有地址和自动更新(内核靠文件监视跟进),详情页照此展示, 编辑按钮禁用。ini 导入的取文件按钮换成同一个拖放模块。 订阅的更新间隔改成开关语义:内核只在 interval > 0 时才起定时拉取 (component/resource/fetcher.go:226-228),关掉开关就是不写这个键。 配置页加「更新所有订阅」,详情的「立刻更新」改叫「更新订阅」。 规则表从零开始写入时自动垫上内网直连——保留地址不直连的话第一条 代理规则一生效,访问局域网的流量就全进了代理;按钮改成「补全内网 直连」,只在有缺口时出现。 所有表单对话框点遮罩关闭;失败横幅右侧加关闭按钮。
This commit is contained in:
+69
-9
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user