规则编辑:建议列表全量展开,目标补齐候选,删除免确认
组合框展开先给全量、本次展开输入过才过滤——按当前值过滤的话,预填的 类型会把列表滤到只剩它自己,MATCH 这类排在后面的选项等于不存在。类型 因此可以默认留空,placeholder 当示例。 目标建议由页面传入:全局规则给内置两个加用户自建的组和自有线路;附加 规则多一项本路专属组——「这个载荷走本路代理」写的就是它,放在拦截规则 前面还能做豁免。生成的专属组不列,每路三个会把列表灌成一面墙。 原文/字段切换只拦真正整行编辑的类型(isRawOnlyRule):之前用 isStructuredEditable 拦,新增时的空原文永远过不了,卡死在原文模式。 删除规则和附加规则不再二次确认——删错了有撤回,确认框只是多一步; toast 带上被删规则的原文。
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
firstMatchIndex,
|
firstMatchIndex,
|
||||||
inferRuleFormat,
|
inferRuleFormat,
|
||||||
insertBeforeMatch,
|
insertBeforeMatch,
|
||||||
|
isRawOnlyRule,
|
||||||
isStructuredEditable,
|
isStructuredEditable,
|
||||||
missingLanRules,
|
missingLanRules,
|
||||||
moveRule,
|
moveRule,
|
||||||
@@ -82,6 +83,13 @@ describe('规则字符串的拆分与拼装', () => {
|
|||||||
expect(isStructuredEditable('DOMAIN-REGEX,^ads?,b$,REJECT')).toBe(false)
|
expect(isStructuredEditable('DOMAIN-REGEX,^ads?,b$,REJECT')).toBe(false)
|
||||||
expect(isStructuredEditable('孤零零一段')).toBe(false)
|
expect(isStructuredEditable('孤零零一段')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('只认整行编辑的类型;空行和没写完的行不算,表单要能接回去', () => {
|
||||||
|
expect(isRawOnlyRule('AND,((DOMAIN-SUFFIX,a.com),(DST-PORT,443)),PROXY')).toBe(true)
|
||||||
|
expect(isRawOnlyRule('DOMAIN-REGEX,^ads?,b$,REJECT')).toBe(true)
|
||||||
|
expect(isRawOnlyRule('')).toBe(false)
|
||||||
|
expect(isRawOnlyRule('DOMAIN,youtube.com')).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('规则表的顺序', () => {
|
describe('规则表的顺序', () => {
|
||||||
|
|||||||
+7
-3
@@ -58,10 +58,14 @@ const RAW_ONLY_TYPES = new Set([
|
|||||||
'PROCESS-PATH-REGEX',
|
'PROCESS-PATH-REGEX',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/** 这条规则是不是只能按原文整行编辑的类型(逻辑规则、载荷可含逗号的正则类)。 */
|
||||||
|
export function isRawOnlyRule(rule: string): boolean {
|
||||||
|
return RAW_ONLY_TYPES.has(parseRule(rule).type.toUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
export function isStructuredEditable(rule: string): boolean {
|
export function isStructuredEditable(rule: string): boolean {
|
||||||
const parsed = parseRule(rule)
|
if (isRawOnlyRule(rule)) return false
|
||||||
if (RAW_ONLY_TYPES.has(parsed.type.toUpperCase())) return false
|
return parseRule(rule).target !== null
|
||||||
return parsed.target !== null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 第一条 MATCH 的下标。它之后的规则永远到不了。没有 MATCH 返回 -1。 */
|
/** 第一条 MATCH 的下标。它之后的规则永远到不了。没有 MATCH 返回 -1。 */
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
* 手填任何值都合法(规则的目标可以是候选之外的出站名)。换掉 datalist
|
* 手填任何值都合法(规则的目标可以是候选之外的出站名)。换掉 datalist
|
||||||
* 的理由和 SelectBox 一样:原生建议列表的样式、动画、开合状态都管不到。
|
* 的理由和 SelectBox 一样:原生建议列表的样式、动画、开合状态都管不到。
|
||||||
*
|
*
|
||||||
|
* 过滤只在本次展开后输入过才生效:带着已有值展开时按值过滤的话,
|
||||||
|
* 列表只剩当前值自己,其它选项等于不存在。
|
||||||
|
*
|
||||||
* Enter 的归属:弹层开着且有高亮项时选中它、不提交表单;弹层收起时不拦,
|
* Enter 的归属:弹层开着且有高亮项时选中它、不提交表单;弹层收起时不拦,
|
||||||
* 交给对话框的表单提交——填完直接回车保存是原生 input 的习惯,不能破坏。
|
* 交给对话框的表单提交——填完直接回车保存是原生 input 的习惯,不能破坏。
|
||||||
*/
|
*/
|
||||||
@@ -30,10 +33,12 @@ const open = ref(false)
|
|||||||
const openUp = ref(false)
|
const openUp = ref(false)
|
||||||
const active = ref(0)
|
const active = ref(0)
|
||||||
const menuStyle = ref<Record<string, string>>({})
|
const menuStyle = ref<Record<string, string>>({})
|
||||||
|
/** 本次展开后是否输入过。没输入过就给全量列表 */
|
||||||
|
const typed = ref(false)
|
||||||
|
|
||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
const q = text.value.trim().toLowerCase()
|
const q = text.value.trim().toLowerCase()
|
||||||
if (!q) return props.options
|
if (!typed.value || !q) return props.options
|
||||||
return props.options.filter((name) => name.toLowerCase().includes(q))
|
return props.options.filter((name) => name.toLowerCase().includes(q))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -80,13 +85,17 @@ onBeforeUnmount(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
|
typed.value = false
|
||||||
active.value = 0
|
active.value = 0
|
||||||
place()
|
place()
|
||||||
open.value = true
|
open.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInput() {
|
function onInput() {
|
||||||
show()
|
typed.value = true
|
||||||
|
active.value = 0
|
||||||
|
place()
|
||||||
|
open.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFromChevron() {
|
function toggleFromChevron() {
|
||||||
|
|||||||
+24
-18
@@ -7,13 +7,14 @@
|
|||||||
* 运行时动作(切换选中、测延迟)不经过它,因为它们不写文件也不需要校验。
|
* 运行时动作(切换选中、测延迟)不经过它,因为它们不写文件也不需要校验。
|
||||||
*/
|
*/
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { collectInbounds } from '@shared/inbounds.ts'
|
import { collectInbounds, collectOutboundNames } from '@shared/inbounds.ts'
|
||||||
import type { Inbound } from '@shared/inbounds.ts'
|
import type { Inbound } from '@shared/inbounds.ts'
|
||||||
import {
|
import {
|
||||||
applySetup,
|
applySetup,
|
||||||
applySubRules,
|
applySubRules,
|
||||||
chainGroupName,
|
chainGroupName,
|
||||||
groupUses,
|
groupUses,
|
||||||
|
isGenerated,
|
||||||
panelGroups,
|
panelGroups,
|
||||||
proxyGroupName,
|
proxyGroupName,
|
||||||
pruneGenerated,
|
pruneGenerated,
|
||||||
@@ -233,11 +234,24 @@ const subRules = computed(() =>
|
|||||||
/** RULE-SET 载荷的候选,给规则表单用 */
|
/** RULE-SET 载荷的候选,给规则表单用 */
|
||||||
const ruleProviderNames = computed(() => Object.keys(ruleProvidersOf(props.data)))
|
const ruleProviderNames = computed(() => Object.keys(ruleProvidersOf(props.data)))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加规则目标的建议:日常用的两个内置出站、这一路自己的专属组(「这个载荷走
|
||||||
|
* 本路代理」写的就是它),再加用户自己建的策略组和自有线路。别的入站的专属组不列。
|
||||||
|
*/
|
||||||
|
const subTargetOptions = computed(() => {
|
||||||
|
const outs = collectOutboundNames(props.data)
|
||||||
|
return [
|
||||||
|
'DIRECT',
|
||||||
|
'REJECT',
|
||||||
|
...(selected.value ? [proxyGroupName(selected.value.name)] : []),
|
||||||
|
...outs.groups.filter((name) => !isGenerated(name)),
|
||||||
|
...outs.ownProxies,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const subFormOpen = ref(false)
|
const subFormOpen = ref(false)
|
||||||
/** 编辑时是下标,新增时是 null */
|
/** 编辑时是下标,新增时是 null */
|
||||||
const subEditing = ref<number | null>(null)
|
const subEditing = ref<number | null>(null)
|
||||||
/** 待确认删除的附加规则下标 */
|
|
||||||
const removingSubRule = ref<number | null>(null)
|
|
||||||
|
|
||||||
function openSubAdd() {
|
function openSubAdd() {
|
||||||
subEditing.value = null
|
subEditing.value = null
|
||||||
@@ -272,13 +286,13 @@ async function moveSubRule(index: number, delta: -1 | 1) {
|
|||||||
await commitSubRules(moveRule(subRules.value, index, delta), '顺序已保存')
|
await commitSubRules(moveRule(subRules.value, index, delta), '顺序已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeSubRuleConfirmed() {
|
// 删除不设二次确认:删错了有撤回,规则原文放进提示里让人看清删掉的是哪条
|
||||||
const index = removingSubRule.value
|
async function removeSubRule(index: number) {
|
||||||
removingSubRule.value = null
|
const rule = subRules.value[index]
|
||||||
if (index === null) return
|
if (rule === undefined) return
|
||||||
await commitSubRules(
|
await commitSubRules(
|
||||||
subRules.value.filter((_, i) => i !== index),
|
subRules.value.filter((_, i) => i !== index),
|
||||||
'已删除这条附加规则',
|
`已删除附加规则 [ ${rule} ]`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +374,7 @@ async function removeConfirmed() {
|
|||||||
@adopt="adopt"
|
@adopt="adopt"
|
||||||
@sub-add="openSubAdd"
|
@sub-add="openSubAdd"
|
||||||
@sub-edit="openSubEdit"
|
@sub-edit="openSubEdit"
|
||||||
@sub-remove="removingSubRule = $event"
|
@sub-remove="removeSubRule"
|
||||||
@sub-move="moveSubRule"
|
@sub-move="moveSubRule"
|
||||||
@sub-lan="addLanSubRules"
|
@sub-lan="addLanSubRules"
|
||||||
/>
|
/>
|
||||||
@@ -390,20 +404,12 @@ async function removeConfirmed() {
|
|||||||
:open="subFormOpen"
|
:open="subFormOpen"
|
||||||
:editing="subEditing === null ? null : { index: subEditing, raw: subRules[subEditing]! }"
|
:editing="subEditing === null ? null : { index: subEditing, raw: subRules[subEditing]! }"
|
||||||
:provider-names="ruleProviderNames"
|
:provider-names="ruleProviderNames"
|
||||||
|
:target-options="subTargetOptions"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
@close="subFormOpen = false"
|
@close="subFormOpen = false"
|
||||||
@submit="submitSubRule"
|
@submit="submitSubRule"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
:open="removingSubRule !== null"
|
|
||||||
:message="
|
|
||||||
removingSubRule !== null ? `删除附加规则「${subRules[removingSubRule] ?? ''}」?` : ''
|
|
||||||
"
|
|
||||||
action="删除"
|
|
||||||
@close="removingSubRule = null"
|
|
||||||
@confirm="removeSubRuleConfirmed"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+11
-12
@@ -8,7 +8,7 @@
|
|||||||
* 列出来的多得多。
|
* 列出来的多得多。
|
||||||
*/
|
*/
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { buildRule, isStructuredEditable, parseRule } from '@shared/rules.ts'
|
import { buildRule, isRawOnlyRule, isStructuredEditable, parseRule } from '@shared/rules.ts'
|
||||||
import ComboBox from '../components/ComboBox.vue'
|
import ComboBox from '../components/ComboBox.vue'
|
||||||
import IconClose from '~icons/ri/close-line'
|
import IconClose from '~icons/ri/close-line'
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ const props = defineProps<{
|
|||||||
editing: { index: number; raw: string } | null
|
editing: { index: number; raw: string } | null
|
||||||
/** RULE-SET 载荷的候选:已声明的规则组 */
|
/** RULE-SET 载荷的候选:已声明的规则组 */
|
||||||
providerNames: string[]
|
providerNames: string[]
|
||||||
|
/** 目标的建议。只是建议,手填任何出站名都合法 */
|
||||||
|
targetOptions: string[]
|
||||||
saving: boolean
|
saving: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -67,16 +69,10 @@ const RULE_TYPES = [
|
|||||||
'MATCH',
|
'MATCH',
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
|
||||||
* 目标的建议只给内置出站里日常用的两个。策略组名允许手填——
|
|
||||||
* 全部列出来的话,每路入站自动生成的三个专属组会把列表灌成一面墙。
|
|
||||||
*/
|
|
||||||
const TARGET_SUGGESTIONS = ['DIRECT', 'REJECT']
|
|
||||||
|
|
||||||
const dialog = ref<HTMLDialogElement | null>(null)
|
const dialog = ref<HTMLDialogElement | null>(null)
|
||||||
const rawMode = ref(false)
|
const rawMode = ref(false)
|
||||||
const raw = ref('')
|
const raw = ref('')
|
||||||
const type = ref('DOMAIN-SUFFIX')
|
const type = ref('')
|
||||||
const payload = ref('')
|
const payload = ref('')
|
||||||
const target = ref('')
|
const target = ref('')
|
||||||
const extras = ref('')
|
const extras = ref('')
|
||||||
@@ -115,7 +111,8 @@ watch(
|
|||||||
} else {
|
} else {
|
||||||
rawMode.value = false
|
rawMode.value = false
|
||||||
const parsed = parseRule(source)
|
const parsed = parseRule(source)
|
||||||
type.value = parsed.type || 'DOMAIN-SUFFIX'
|
// 新增时留空:预填的类型会把下拉的建议过滤到只剩它自己
|
||||||
|
type.value = parsed.type
|
||||||
payload.value = parsed.payload ?? ''
|
payload.value = parsed.payload ?? ''
|
||||||
target.value = parsed.target ?? ''
|
target.value = parsed.target ?? ''
|
||||||
extras.value = parsed.extras.join(',')
|
extras.value = parsed.extras.join(',')
|
||||||
@@ -131,7 +128,9 @@ watch(
|
|||||||
function toggleMode() {
|
function toggleMode() {
|
||||||
if (rawMode.value) {
|
if (rawMode.value) {
|
||||||
const line = raw.value.trim()
|
const line = raw.value.trim()
|
||||||
if (!isStructuredEditable(line)) return
|
// 只有逻辑规则这类整行编辑的类型留在原文模式;空行、没写完的行都带回表单
|
||||||
|
// 接着填——按 isStructuredEditable 拦的话,新增时的空原文会让人永远切不回去
|
||||||
|
if (isRawOnlyRule(line)) return
|
||||||
const parsed = parseRule(line)
|
const parsed = parseRule(line)
|
||||||
type.value = parsed.type
|
type.value = parsed.type
|
||||||
payload.value = parsed.payload ?? ''
|
payload.value = parsed.payload ?? ''
|
||||||
@@ -189,7 +188,7 @@ function onBackdrop(event: MouseEvent) {
|
|||||||
<label class="row">
|
<label class="row">
|
||||||
<span class="k">类型</span>
|
<span class="k">类型</span>
|
||||||
<span class="v">
|
<span class="v">
|
||||||
<ComboBox v-model="type" mono :options="RULE_TYPES" />
|
<ComboBox v-model="type" mono :options="RULE_TYPES" placeholder="DOMAIN-SUFFIX" />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -218,7 +217,7 @@ function onBackdrop(event: MouseEvent) {
|
|||||||
<label class="row">
|
<label class="row">
|
||||||
<span class="k">目标</span>
|
<span class="k">目标</span>
|
||||||
<span class="v">
|
<span class="v">
|
||||||
<ComboBox v-model="target" mono :options="TARGET_SUGGESTIONS" placeholder="DIRECT" />
|
<ComboBox v-model="target" mono :options="targetOptions" placeholder="DIRECT" />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
+19
-20
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { collectOutboundNames } from '@shared/inbounds.ts'
|
import { collectOutboundNames } from '@shared/inbounds.ts'
|
||||||
|
import { isGenerated } from '@shared/inbound-groups.ts'
|
||||||
import {
|
import {
|
||||||
findRuleIssues,
|
findRuleIssues,
|
||||||
firstMatchIndex,
|
firstMatchIndex,
|
||||||
@@ -76,6 +77,17 @@ const outboundNames = computed(() => [
|
|||||||
...outbounds.value.ownProxies,
|
...outbounds.value.ownProxies,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标的建议:日常用的两个内置出站、用户自己建的策略组和自有线路。
|
||||||
|
* 生成的专属组不列——每路入站三个,会把列表灌成一面墙,真要指定就手填。
|
||||||
|
*/
|
||||||
|
const targetOptions = computed(() => [
|
||||||
|
'DIRECT',
|
||||||
|
'REJECT',
|
||||||
|
...outbounds.value.groups.filter((name) => !isGenerated(name)),
|
||||||
|
...outbounds.value.ownProxies,
|
||||||
|
])
|
||||||
|
|
||||||
/** 第一条 MATCH 的下标。它之后的行标成不可达。 */
|
/** 第一条 MATCH 的下标。它之后的行标成不可达。 */
|
||||||
const matchAt = computed(() => firstMatchIndex(rules.value))
|
const matchAt = computed(() => firstMatchIndex(rules.value))
|
||||||
|
|
||||||
@@ -161,17 +173,11 @@ async function move(index: number, delta: -1 | 1) {
|
|||||||
await commit(withRules(moveRule(rules.value, index, delta)), '顺序已保存')
|
await commit(withRules(moveRule(rules.value, index, delta)), '顺序已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 待确认删除的规则下标。非 null 时确认对话框打开着。 */
|
// 删除不设二次确认:删错了有撤回,规则原文放进提示里让人看清删掉的是哪条
|
||||||
const removingRule = ref<number | null>(null)
|
async function removeRule(index: number) {
|
||||||
|
const rule = rules.value[index]
|
||||||
async function removeRuleConfirmed() {
|
if (rule === undefined || saving.value) return
|
||||||
const index = removingRule.value
|
await commit(withRules(rules.value.filter((_, i) => i !== index)), `已删除规则 [ ${rule} ]`)
|
||||||
removingRule.value = null
|
|
||||||
if (index === null || saving.value) return
|
|
||||||
await commit(
|
|
||||||
withRules(rules.value.filter((_, i) => i !== index)),
|
|
||||||
'已删除这条规则',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyLanPreset() {
|
async function applyLanPreset() {
|
||||||
@@ -429,7 +435,7 @@ async function updateNow(name: string) {
|
|||||||
class="icon-btn danger"
|
class="icon-btn danger"
|
||||||
title="删除"
|
title="删除"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
@click="removingRule = row.index"
|
@click="removeRule(row.index)"
|
||||||
>
|
>
|
||||||
<IconDelete />
|
<IconDelete />
|
||||||
</button>
|
</button>
|
||||||
@@ -493,6 +499,7 @@ async function updateNow(name: string) {
|
|||||||
:open="ruleFormOpen"
|
:open="ruleFormOpen"
|
||||||
:editing="ruleEditing === null ? null : { index: ruleEditing, raw: rules[ruleEditing]! }"
|
:editing="ruleEditing === null ? null : { index: ruleEditing, raw: rules[ruleEditing]! }"
|
||||||
:provider-names="providerNames"
|
:provider-names="providerNames"
|
||||||
|
:target-options="targetOptions"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
@close="ruleFormOpen = false"
|
@close="ruleFormOpen = false"
|
||||||
@submit="submitRule"
|
@submit="submitRule"
|
||||||
@@ -515,14 +522,6 @@ async function updateNow(name: string) {
|
|||||||
@submit="submitImport"
|
@submit="submitImport"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
:open="removingRule !== null"
|
|
||||||
:message="removingRule === null ? '' : `删除这条规则?${rules[removingRule]}`"
|
|
||||||
action="删除"
|
|
||||||
@close="removingRule = null"
|
|
||||||
@confirm="removeRuleConfirmed"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
:open="removingProvider !== null"
|
:open="removingProvider !== null"
|
||||||
:message="removingProvider === null ? '' : `删除规则组「${removingProvider}」?`"
|
:message="removingProvider === null ? '' : `删除规则组「${removingProvider}」?`"
|
||||||
|
|||||||
Reference in New Issue
Block a user