规则页:全局分流规则的编辑与规则组管理
新增 RulesPage.vue 和规则、规则组、ini 导入三个对话框,纯逻辑放在 shared/rules.ts。拆分规则字符串复用 vendor 的 splitRuleFields——引号和 嵌套括号里的逗号不会被当成字段分隔;目标之后的选项段原样拼回,改一条 规则不会丢掉它的 no-resolve 尾巴。规则组的缓存路径显式生成:不写 path 时内核按 url 哈希自己算一个,那个路径这里算不出来。
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 新增一个规则组(rule-providers 里的一条声明)。
|
||||
*
|
||||
* 内容的拉取、缓存、定时更新全是内核的活,这里只写声明:URL 生成 http 类型,
|
||||
* 本地路径生成 file 类型,缓存路径自动补一个确定的——不写 path 时内核按 url
|
||||
* 哈希自己生成,那个路径这里算不出来。
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { inferRuleFormat, ruleProviderPath } from '@shared/rules.ts'
|
||||
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
|
||||
import IconClose from '~icons/ri/close-line'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
/** 已被占用的规则组名 */
|
||||
takenNames: string[]
|
||||
saving: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
submit: [entry: { name: string; declaration: ConfigObject }]
|
||||
}>()
|
||||
|
||||
const dialog = ref<HTMLDialogElement | null>(null)
|
||||
const name = ref('')
|
||||
const source = ref('')
|
||||
const behavior = ref('classical')
|
||||
const format = ref<'yaml' | 'text' | 'mrs'>('yaml')
|
||||
/** 用户手选过格式就不再跟着来源推断 */
|
||||
const formatTouched = ref(false)
|
||||
|
||||
const isUrl = computed(() => /^https?:\/\//i.test(source.value.trim()))
|
||||
|
||||
watch(source, (value) => {
|
||||
if (!formatTouched.value) format.value = inferRuleFormat(value.trim())
|
||||
})
|
||||
|
||||
const nameTaken = computed(() => props.takenNames.includes(name.value.trim()))
|
||||
|
||||
/** 内核没有实现 classical 行为的 mrs 格式(rules/provider/ 下只有 domain 和 ipcidr 两种策略) */
|
||||
const mrsConflict = computed(() => format.value === 'mrs' && behavior.value === 'classical')
|
||||
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
name.value.trim() !== '' &&
|
||||
source.value.trim() !== '' &&
|
||||
!nameTaken.value &&
|
||||
!mrsConflict.value &&
|
||||
!props.saving,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) {
|
||||
name.value = ''
|
||||
source.value = ''
|
||||
behavior.value = 'classical'
|
||||
format.value = 'yaml'
|
||||
formatTouched.value = false
|
||||
dialog.value?.showModal()
|
||||
} else {
|
||||
dialog.value?.close()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function submit() {
|
||||
if (!canSubmit.value) return
|
||||
const trimmedName = name.value.trim()
|
||||
const trimmedSource = source.value.trim()
|
||||
const declaration: ConfigObject = isUrl.value
|
||||
? {
|
||||
type: 'http',
|
||||
behavior: behavior.value,
|
||||
url: trimmedSource,
|
||||
path: ruleProviderPath(trimmedName, format.value),
|
||||
format: format.value,
|
||||
interval: 86400,
|
||||
}
|
||||
: { type: 'file', behavior: behavior.value, path: trimmedSource, format: format.value }
|
||||
emit('submit', { name: trimmedName, declaration })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog ref="dialog" class="dlg" @close="emit('close')" @cancel.prevent="emit('close')">
|
||||
<form method="dialog" @submit.prevent="submit">
|
||||
<header class="dlg-head">
|
||||
<h2>新增规则组</h2>
|
||||
<button type="button" class="x" @click="emit('close')"><IconClose /></button>
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<label class="row">
|
||||
<span class="k">名称</span>
|
||||
<span class="v">
|
||||
<input v-model="name" type="text" placeholder="ads" autocomplete="off" />
|
||||
<span v-if="nameTaken" class="bad">名称已存在</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span class="k">来源</span>
|
||||
<span class="v">
|
||||
<input
|
||||
v-model="source"
|
||||
class="mono"
|
||||
type="text"
|
||||
placeholder="https://… 或本地文件路径"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<span v-if="source.trim() !== ''" class="note">
|
||||
{{ isUrl ? 'http 类型,由内核定时拉取' : 'file 类型,读本机文件' }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span class="k">行为</span>
|
||||
<span class="v">
|
||||
<select v-model="behavior">
|
||||
<option value="classical">classical</option>
|
||||
<option value="domain">domain</option>
|
||||
<option value="ipcidr">ipcidr</option>
|
||||
</select>
|
||||
<span class="note">
|
||||
清单每行带 DOMAIN,、IP-CIDR, 这类前缀的是 classical;纯域名列表是 domain;
|
||||
纯 IP 网段列表是 ipcidr
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span class="k">格式</span>
|
||||
<span class="v">
|
||||
<select v-model="format" @change="formatTouched = true">
|
||||
<option value="yaml">yaml</option>
|
||||
<option value="text">text</option>
|
||||
<option value="mrs">mrs</option>
|
||||
</select>
|
||||
<span v-if="mrsConflict" class="bad">mrs 格式只支持 domain 和 ipcidr 两种行为</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<footer class="dlg-foot">
|
||||
<button type="button" class="btn" @click="emit('close')">取消</button>
|
||||
<button type="submit" class="btn btn-go" :disabled="!canSubmit">
|
||||
{{ saving ? '保存中' : '保存' }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dlg {
|
||||
width: min(520px, calc(100vw - 2 * var(--s-5)));
|
||||
padding: 0;
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--c-surface);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.dlg::backdrop {
|
||||
background: rgb(0 0 0 / 0.45);
|
||||
}
|
||||
|
||||
.dlg-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-4) var(--s-5);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.dlg-head h2 {
|
||||
font-size: var(--t-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.x {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
color: var(--c-text-faint);
|
||||
cursor: pointer;
|
||||
}
|
||||
.x:hover {
|
||||
background: var(--c-surface-sunken);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-4);
|
||||
padding: var(--s-5);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr;
|
||||
gap: var(--s-4);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.k {
|
||||
color: var(--c-text-soft);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: var(--s-2) var(--s-3);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
padding-right: var(--s-6);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16'%3E%3Cpath fill='%23898f98' d='M12 15.6 5.7 9.3l1.4-1.4 4.9 4.9 4.9-4.9 1.4 1.4z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right var(--s-3) center;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: var(--t-xs);
|
||||
color: var(--c-text-faint);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bad {
|
||||
font-size: var(--t-xs);
|
||||
color: var(--c-danger);
|
||||
}
|
||||
|
||||
.dlg-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--s-2);
|
||||
padding: var(--s-4) var(--s-5);
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--s-2) var(--s-4);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--c-surface);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-go {
|
||||
background: var(--c-accent);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-go:disabled {
|
||||
background: var(--c-surface-sunken);
|
||||
border-color: var(--c-border);
|
||||
color: var(--c-text-faint);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user