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