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

文件导入走两步:内容先经新端点 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
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts">
/**
* 拖拽或点选一个文件,读出文本内容交给调用方。
*
* 无状态:只发 load 事件,选没选、选了什么由调用方自己记——三个用它的表单
* 对「选中之后长什么样」要求各不相同(订阅显示文件名待提交,ini 导入直接
* 灌进文本框),状态放这里反而要为每种用法开口子。
*/
import { ref } from 'vue'
import IconUpload from '~icons/ri/upload-cloud-2-line'
const props = defineProps<{
/** 原生 accept 串,如 ".yaml,.yml,.txt" */
accept?: string
}>()
const emit = defineEmits<{ load: [name: string, content: string] }>()
const input = ref<HTMLInputElement | null>(null)
const dragging = ref(false)
async function take(file: File | undefined) {
if (!file) return
emit('load', file.name, await file.text())
}
function onDrop(event: DragEvent) {
dragging.value = false
void take(event.dataTransfer?.files[0])
}
function onPick(event: Event) {
const el = event.target as HTMLInputElement
void take(el.files?.[0])
// 同一个文件选第二次也要触发 change
el.value = ''
}
</script>
<template>
<button
type="button"
:class="['zone', dragging && 'is-over']"
@click="input?.click()"
@dragover.prevent="dragging = true"
@dragleave="dragging = false"
@drop.prevent="onDrop"
>
<IconUpload class="ico" aria-hidden="true" />
<span>拖拽文件到这里,或点击选择</span>
<input ref="input" type="file" :accept="props.accept" hidden @change="onPick" />
</button>
</template>
<style scoped>
.zone {
display: flex;
align-items: center;
justify-content: center;
gap: var(--s-2);
width: 100%;
padding: var(--s-4);
border: 1px dashed var(--c-border-strong);
border-radius: var(--r-sm);
background: none;
color: var(--c-text-soft);
font-size: var(--t-sm);
cursor: pointer;
user-select: none;
transition:
border-color 0.15s ease,
color 0.15s ease,
background-color 0.15s ease;
}
.zone:hover,
.zone.is-over {
border-color: var(--c-accent);
color: var(--c-accent-text);
background: var(--c-accent-soft);
}
.ico {
width: 18px;
height: 18px;
}
</style>