init: init proj
This commit is contained in:
266
web/src/pages/RulesPage.vue
Normal file
266
web/src/pages/RulesPage.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<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: 'Proxy', params: [], enabled: true });
|
||||
|
||||
const policyOptions = computed(() => [
|
||||
...['DIRECT', 'REJECT', 'REJECT-DROP', 'REJECT-NO-DROP'].map((p) => ({ label: p, value: p })),
|
||||
...groupsStore.groups.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: 'Proxy', 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, 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, onClick: () => move(row, -1) }, () => '↑'),
|
||||
h(NButton, { size: 'tiny', quaternary: true, disabled: i === store.rules.length - 1, 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'
|
||||
? [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>
|
||||
Reference in New Issue
Block a user