迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
267 lines
7.7 KiB
Vue
267 lines
7.7 KiB
Vue
<script setup lang="ts">
|
||
import { computed, h, onMounted, ref } from 'vue';
|
||
import {
|
||
NButton,
|
||
NDataTable,
|
||
NModal,
|
||
NForm,
|
||
NFormItem,
|
||
NInput,
|
||
NSelect,
|
||
NSpace,
|
||
NSwitch,
|
||
NTag,
|
||
useDialog,
|
||
useMessage,
|
||
type DataTableColumns,
|
||
} from 'naive-ui';
|
||
import { RULE_TYPES, type RuleDto, type RuleInput } from '@proxy-station/shared';
|
||
import { useRulesStore } from '../stores/rules';
|
||
import { useGroupsStore } from '../stores/groups';
|
||
import RulesetPreviewDrawer from '../components/RulesetPreviewDrawer.vue';
|
||
|
||
const store = useRulesStore();
|
||
const groupsStore = useGroupsStore();
|
||
const message = useMessage();
|
||
const dialog = useDialog();
|
||
|
||
onMounted(() => {
|
||
store.fetchAll();
|
||
groupsStore.fetchAll();
|
||
});
|
||
|
||
const showForm = ref(false);
|
||
const editing = ref<RuleDto | null>(null);
|
||
const previewRule = ref<RuleDto | null>(null);
|
||
const showPreview = ref(false);
|
||
|
||
function openPreview(rule: RuleDto) {
|
||
previewRule.value = rule;
|
||
showPreview.value = true;
|
||
}
|
||
const form = ref<RuleInput>({ type: 'DOMAIN-SUFFIX', value: '', policy: 'DIRECT', params: [], enabled: true });
|
||
|
||
const policyOptions = computed(() => [
|
||
...['DIRECT', 'REJECT', 'REJECT-DROP', 'REJECT-NO-DROP'].map((p) => ({ label: p, value: p })),
|
||
...groupsStore.groups.filter((g) => g.enabled).map((g) => ({ label: g.name, value: g.name })),
|
||
]);
|
||
|
||
const typeOptions = RULE_TYPES.filter((t) => t !== 'RULE-SET' && t !== 'DOMAIN-SET' && t !== 'FINAL').map((t) => ({
|
||
label: t,
|
||
value: t,
|
||
}));
|
||
|
||
function openCreate() {
|
||
editing.value = null;
|
||
form.value = { type: 'DOMAIN-SUFFIX', value: '', policy: 'DIRECT', params: [], enabled: true };
|
||
showForm.value = true;
|
||
}
|
||
|
||
function openEdit(rule: RuleDto) {
|
||
editing.value = rule;
|
||
form.value = {
|
||
type: rule.type as RuleInput['type'],
|
||
value: rule.value,
|
||
policy: rule.policy,
|
||
params: rule.params,
|
||
enabled: rule.enabled,
|
||
};
|
||
showForm.value = true;
|
||
}
|
||
|
||
async function submit() {
|
||
try {
|
||
if (editing.value) await store.update(editing.value.id, form.value);
|
||
else await store.create(form.value);
|
||
showForm.value = false;
|
||
message.success('已保存');
|
||
} catch (e: any) {
|
||
message.error(e.message);
|
||
}
|
||
}
|
||
|
||
function confirmDelete(rule: RuleDto) {
|
||
dialog.warning({
|
||
title: '删除规则',
|
||
content: `确认删除这条 ${rule.type} 规则?`,
|
||
positiveText: '删除',
|
||
negativeText: '取消',
|
||
onPositiveClick: () => store.remove(rule.id),
|
||
});
|
||
}
|
||
|
||
function confirmReset() {
|
||
dialog.warning({
|
||
title: '重置为模板规则',
|
||
content: '模板来源的规则将恢复为初始状态,你添加的自定义规则会保留。继续?',
|
||
positiveText: '重置',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
await store.reset();
|
||
message.success('已重置');
|
||
},
|
||
});
|
||
}
|
||
|
||
async function move(rule: RuleDto, dir: -1 | 1) {
|
||
const ids = store.rules.map((r) => r.id);
|
||
const i = ids.indexOf(rule.id);
|
||
const j = i + dir;
|
||
if (j < 0 || j >= ids.length) return;
|
||
[ids[i], ids[j]] = [ids[j], ids[i]];
|
||
await store.reorder(ids);
|
||
}
|
||
|
||
async function changePolicy(rule: RuleDto, policy: string) {
|
||
await store.update(rule.id, {
|
||
type: rule.type as RuleInput['type'],
|
||
value: rule.value,
|
||
policy,
|
||
params: rule.params,
|
||
enabled: rule.enabled,
|
||
});
|
||
}
|
||
|
||
const columns = computed<DataTableColumns<RuleDto>>(() => [
|
||
{ title: '#', key: 'seq', width: 56, render: (_row, i) => h('span', { class: 'mono', style: 'color:var(--muted);font-size:12px' }, String(i + 1)) },
|
||
{
|
||
title: '类型',
|
||
key: 'type',
|
||
width: 130,
|
||
render: (row) =>
|
||
h(
|
||
NTag,
|
||
{ size: 'small', bordered: false, type: row.type === 'RULE-SET' || row.type === 'DOMAIN-SET' ? 'info' : 'default' },
|
||
() => row.type
|
||
),
|
||
},
|
||
{
|
||
title: '匹配值',
|
||
key: 'value',
|
||
ellipsis: { tooltip: true },
|
||
render: (row) =>
|
||
row.rulesetName
|
||
? h('span', { style: 'cursor:pointer', onClick: () => openPreview(row), title: '点击预览规则集内容' }, [
|
||
h('span', { style: 'text-decoration:underline dotted var(--muted)' }, row.rulesetName),
|
||
h('span', { class: 'mono', style: 'color:var(--muted);font-size:11px;margin-left:8px' }, row.rulesetUrl ?? ''),
|
||
])
|
||
: h('span', { class: 'mono', style: 'font-size:12px' }, row.value ?? '—'),
|
||
},
|
||
{
|
||
title: '策略',
|
||
key: 'policy',
|
||
width: 170,
|
||
render: (row) =>
|
||
h(NSelect, {
|
||
size: 'small',
|
||
value: row.policy,
|
||
options: policyOptions.value,
|
||
consistentMenuWidth: false,
|
||
onUpdateValue: (v: string) => changePolicy(row, v),
|
||
}),
|
||
},
|
||
{
|
||
title: '来源',
|
||
key: 'source',
|
||
width: 76,
|
||
render: (row) => h('span', { style: 'color:var(--muted);font-size:12px' }, row.source === 'template' ? '模板' : '自定义'),
|
||
},
|
||
{
|
||
title: '启用',
|
||
key: 'enabled',
|
||
width: 70,
|
||
render: (row) => h(NSwitch, { size: 'small', value: row.enabled, disabled: row.type === 'FINAL', onUpdateValue: (v: boolean) => store.toggle(row.id, v) }),
|
||
},
|
||
{
|
||
title: '',
|
||
key: 'actions',
|
||
width: 170,
|
||
render: (row, i) =>
|
||
h(NSpace, { size: 2 }, () => [
|
||
h(NButton, { size: 'tiny', quaternary: true, disabled: i === 0 || row.type === 'FINAL', onClick: () => move(row, -1) }, () => '↑'),
|
||
h(NButton, { size: 'tiny', quaternary: true, disabled: i >= store.rules.length - 2 || row.type === 'FINAL', onClick: () => move(row, 1) }, () => '↓'),
|
||
...(row.type !== 'RULE-SET' && row.type !== 'DOMAIN-SET' && row.type !== 'FINAL'
|
||
? [h(NButton, { size: 'tiny', quaternary: true, onClick: () => openEdit(row) }, () => '编辑')]
|
||
: []),
|
||
...(row.source === 'custom' && row.type !== 'FINAL'
|
||
? [h(NButton, { size: 'tiny', quaternary: true, type: 'error', onClick: () => confirmDelete(row) }, () => '删除')]
|
||
: []),
|
||
]),
|
||
},
|
||
]);
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<div class="page-head">
|
||
<h1 class="display page-title">规则</h1>
|
||
<NSpace>
|
||
<NButton ghost @click="confirmReset">重置为模板</NButton>
|
||
<NButton type="primary" @click="openCreate">添加规则</NButton>
|
||
</NSpace>
|
||
</div>
|
||
<p class="hint">规则自上而下匹配。RULE-SET/DOMAIN-SET 引用外部规则集,点击名称可预览内容;末尾 FINAL 是兜底策略。</p>
|
||
|
||
<NDataTable
|
||
:columns="columns"
|
||
:data="store.rules"
|
||
:loading="store.loading"
|
||
:bordered="false"
|
||
size="small"
|
||
:row-key="(r: RuleDto) => r.id"
|
||
:max-height="640"
|
||
virtual-scroll
|
||
/>
|
||
|
||
<RulesetPreviewDrawer v-model:show="showPreview" :rule="previewRule" />
|
||
|
||
<NModal
|
||
:show="showForm"
|
||
preset="card"
|
||
:title="editing ? '编辑规则' : '添加自定义规则'"
|
||
style="max-width: 480px"
|
||
@update:show="showForm = $event"
|
||
>
|
||
<NForm label-placement="left" label-width="72">
|
||
<NFormItem label="类型">
|
||
<NSelect v-model:value="form.type" :options="typeOptions" />
|
||
</NFormItem>
|
||
<NFormItem label="匹配值">
|
||
<NInput v-model:value="form.value" placeholder="如 example.com / 1.2.3.0/24" class="mono" />
|
||
</NFormItem>
|
||
<NFormItem label="策略">
|
||
<NSelect v-model:value="form.policy" :options="policyOptions" filterable />
|
||
</NFormItem>
|
||
</NForm>
|
||
<template #footer>
|
||
<NSpace justify="end">
|
||
<NButton @click="showForm = false">取消</NButton>
|
||
<NButton type="primary" @click="submit">保存</NButton>
|
||
</NSpace>
|
||
</template>
|
||
</NModal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 20px;
|
||
margin: 0;
|
||
}
|
||
|
||
.hint {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
margin: 0 0 16px;
|
||
}
|
||
</style>
|