U:Vue布局实现
This commit is contained in:
1 parent
60d6941074
commit
c55bf3c9ca
4 files changed
+1422
-200
No files matched your search
@@ -1,2 +1,5 @@
|
||||
VITE_MODE='development'
|
||||
VITE_APP_API_URL='http://localhost:8268/api'
|
||||
|
||||
我想让FirewallRuleForm.vue的滚动区域,只在页面主容器内进行,比如“基础内容”很多,那么就在“基础内容”的
|
||||
子Tab中进行滚动,不能超出去,不然滚动时将会整体滚动,非常不优雅,而且你这次优化的样式我觉得还有提升空间,你可以参考MonitorView下的组件进行样式优化
|
||||
+3
-1
@@ -9,14 +9,16 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CommandManager: typeof import('./src/components/CommandManager.vue')['default']
|
||||
FirewallRuleForm: typeof import('./src/components/FirewallRuleForm.vue')['default']
|
||||
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
|
||||
IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
|
||||
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
|
||||
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
|
||||
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
|
||||
NetworkConnectionPanel2: typeof import('./src/components/NetworkConnectionPanel2.vue')['default']
|
||||
NHighlight: typeof import('naive-ui')['NHighlight']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TitleBar: typeof import('./src/components/TitleBar.vue')['default']
|
||||
|
||||
@@ -0,0 +1,956 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
preset="card"
|
||||
:style="{ width: '1000px', height: '90vh' }"
|
||||
:title="isEdit ? '编辑防火墙规则' : '新建防火墙规则'"
|
||||
:closable="true"
|
||||
:auto-focus="false"
|
||||
:mask-closable="false"
|
||||
class="firewall-rule-modal"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="rule-form-container">
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-placement="top"
|
||||
require-mark-placement="right-hanging"
|
||||
class="firewall-form"
|
||||
>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs
|
||||
v-model:value="activeTab"
|
||||
type="line"
|
||||
size="medium"
|
||||
animated
|
||||
class="rule-tabs"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<n-tab-pane name="basic" tab="基本信息">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="InformationCircleOutline" />
|
||||
规则标识
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="name" label="规则名称" required>
|
||||
<n-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="输入唯一的规则名称"
|
||||
maxlength="255"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="description" label="规则描述">
|
||||
<n-input
|
||||
v-model:value="formData.description"
|
||||
type="textarea"
|
||||
placeholder="描述此规则的用途和目的"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="grouping" label="规则分组">
|
||||
<n-input
|
||||
v-model:value="formData.grouping"
|
||||
placeholder="可选:输入分组名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="SettingsOutline" />
|
||||
规则行为
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<n-form-item path="enabled" label="规则状态">
|
||||
<n-radio-group v-model:value="formData.enabled">
|
||||
<n-radio :value="true">启用</n-radio>
|
||||
<n-radio :value="false">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="direction" label="规则方向" required>
|
||||
<n-radio-group v-model:value="formData.direction">
|
||||
<n-radio :value="1">入站 (Inbound)</n-radio>
|
||||
<n-radio :value="2">出站 (Outbound)</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<n-form-item path="action" label="规则动作" required>
|
||||
<n-radio-group v-model:value="formData.action">
|
||||
<n-radio :value="1">允许 (Allow)</n-radio>
|
||||
<n-radio :value="0">阻止 (Block)</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 应用程序和服务 -->
|
||||
<n-tab-pane name="application" tab="应用程序">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="AppsOutline" />
|
||||
应用程序路径
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="applicationName" label="应用程序">
|
||||
<n-input
|
||||
v-model:value="formData.applicationName"
|
||||
placeholder="例: C:\Program Files\Example\app.exe (留空表示所有程序)"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-button text @click="browseApplication">
|
||||
<n-icon :component="FolderOpenOutline" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="serviceName" label="系统服务">
|
||||
<n-select
|
||||
v-model:value="formData.serviceName"
|
||||
:options="serviceOptions"
|
||||
placeholder="选择系统服务 (可选)"
|
||||
filterable
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 协议和端口 -->
|
||||
<n-tab-pane name="protocol" tab="协议端口">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="LayersOutline" />
|
||||
协议设置
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="protocol" label="协议类型" required>
|
||||
<n-select
|
||||
v-model:value="formData.protocol"
|
||||
:options="protocolOptions"
|
||||
placeholder="选择协议类型"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div class="form-row">
|
||||
<n-form-item path="localPorts" label="本地端口">
|
||||
<n-input
|
||||
v-model:value="formData.localPorts"
|
||||
placeholder="例: 80, 443, 8080-8090 (留空表示所有端口)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="remotePorts" label="远程端口">
|
||||
<n-input
|
||||
v-model:value="formData.remotePorts"
|
||||
placeholder="例: 80, 443, 8080-8090 (留空表示所有端口)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<n-form-item
|
||||
v-if="formData.protocol === 1 || formData.protocol === 58"
|
||||
path="icmpTypesAndCodes"
|
||||
label="ICMP 类型和代码"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="formData.icmpTypesAndCodes"
|
||||
placeholder="例: 8:0 (留空表示所有ICMP类型)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 地址设置 -->
|
||||
<n-tab-pane name="address" tab="地址设置">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="GlobeOutline" />
|
||||
IP 地址范围
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="localAddresses" label="本地地址">
|
||||
<n-input
|
||||
v-model:value="formData.localAddresses"
|
||||
placeholder="例: 192.168.1.0/24, 10.0.0.1 (* 表示任何地址)"
|
||||
/>
|
||||
<template #feedback>
|
||||
支持单个IP、CIDR格式或逗号分隔的多个地址
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="remoteAddresses" label="远程地址">
|
||||
<n-input
|
||||
v-model:value="formData.remoteAddresses"
|
||||
placeholder="例: 192.168.1.0/24, 10.0.0.1 (* 表示任何地址)"
|
||||
/>
|
||||
<template #feedback>
|
||||
支持单个IP、CIDR格式或逗号分隔的多个地址
|
||||
</template>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 配置文件和接口 -->
|
||||
<n-tab-pane name="profile" tab="配置文件">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="ShieldCheckmarkOutline" />
|
||||
网络配置文件
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="profiles" label="应用于配置文件" required>
|
||||
<n-checkbox-group v-model:value="formData.profiles">
|
||||
<n-space vertical>
|
||||
<n-checkbox :value="1">
|
||||
<div class="profile-option">
|
||||
<div class="profile-name">域网络 (Domain)</div>
|
||||
<div class="profile-desc">计算机连接到域时</div>
|
||||
</div>
|
||||
</n-checkbox>
|
||||
<n-checkbox :value="2">
|
||||
<div class="profile-option">
|
||||
<div class="profile-name">专用网络 (Private)</div>
|
||||
<div class="profile-desc">家庭或工作网络</div>
|
||||
</div>
|
||||
</n-checkbox>
|
||||
<n-checkbox :value="4">
|
||||
<div class="profile-option">
|
||||
<div class="profile-name">公用网络 (Public)</div>
|
||||
<div class="profile-desc">公共场所的网络</div>
|
||||
</div>
|
||||
</n-checkbox>
|
||||
</n-space>
|
||||
</n-checkbox-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="interfaceTypes" label="接口类型">
|
||||
<n-input
|
||||
v-model:value="formData.interfaceTypes"
|
||||
placeholder="例: Wireless, Lan (留空或'All'表示所有接口)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="interfaces" label="特定接口">
|
||||
<n-dynamic-tags
|
||||
v-model:value="formData.interfaces"
|
||||
placeholder="添加网络接口名称"
|
||||
/>
|
||||
<template #feedback>
|
||||
可以指定特定的网络接口,留空表示所有接口
|
||||
</template>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 高级选项 -->
|
||||
<n-tab-pane name="advanced" tab="高级选项">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="CogOutline" />
|
||||
边缘遍历设置
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="edgeTraversal" label="边缘遍历">
|
||||
<n-checkbox v-model:checked="formData.edgeTraversal">
|
||||
允许边缘遍历 (适用于需要穿越NAT的应用)
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="edgeTraversalAllowed" label="边缘遍历策略">
|
||||
<n-checkbox v-model:checked="formData.edgeTraversalAllowed">
|
||||
允许延迟边缘遍历
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="LockClosedOutline" />
|
||||
授权设置
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="remoteMachineAuthorizationList" label="远程计算机授权">
|
||||
<n-input
|
||||
v-model:value="formData.remoteMachineAuthorizationList"
|
||||
placeholder="输入安全描述符 (高级用户)"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="remoteUserAuthorizationList" label="远程用户授权">
|
||||
<n-input
|
||||
v-model:value="formData.remoteUserAuthorizationList"
|
||||
placeholder="输入安全描述符 (高级用户)"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="embeddedContext" label="嵌入式上下文">
|
||||
<n-input
|
||||
v-model:value="formData.embeddedContext"
|
||||
placeholder="可选的嵌入式上下文信息"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-space>
|
||||
<n-button @click="handleClose">取消</n-button>
|
||||
<n-button @click="handleValidate">验证规则</n-button>
|
||||
<n-button type="primary" @click="handleSave" :loading="saving">
|
||||
<template #icon>
|
||||
<n-icon :component="CheckmarkOutline" />
|
||||
</template>
|
||||
{{ isEdit ? '更新规则' : '创建规则' }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import {
|
||||
NModal,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NRadioGroup,
|
||||
NRadio,
|
||||
NCheckbox,
|
||||
NCheckboxGroup,
|
||||
NDynamicTags,
|
||||
NButton,
|
||||
NIcon,
|
||||
NSpace,
|
||||
FormInst,
|
||||
FormRules,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
CheckmarkOutline,
|
||||
InformationCircleOutline,
|
||||
SettingsOutline,
|
||||
AppsOutline,
|
||||
LayersOutline,
|
||||
GlobeOutline,
|
||||
ShieldCheckmarkOutline,
|
||||
CogOutline,
|
||||
LockClosedOutline,
|
||||
FolderOpenOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
// 枚举定义 - 对应后端模型
|
||||
enum RuleDirection {
|
||||
Inbound = 1,
|
||||
Outbound = 2
|
||||
}
|
||||
|
||||
enum RuleAction {
|
||||
Block = 0,
|
||||
Allow = 1
|
||||
}
|
||||
|
||||
enum ProtocolType {
|
||||
Any = 256,
|
||||
ICMPV4 = 1,
|
||||
IGMP = 2,
|
||||
TCP = 6,
|
||||
UDP = 17,
|
||||
IPv6 = 41,
|
||||
IPv6Route = 43,
|
||||
IPv6Frag = 44,
|
||||
GRE = 47,
|
||||
ICMPv6 = 58,
|
||||
IPv6NoNxt = 59,
|
||||
IPv6Opts = 60,
|
||||
VRRP = 112,
|
||||
PGM = 113,
|
||||
L2TP = 115
|
||||
}
|
||||
|
||||
enum FirewallProfile {
|
||||
Domain = 1,
|
||||
Private = 2,
|
||||
Public = 4
|
||||
}
|
||||
|
||||
// 防火墙规则接口 - 对应后端 FirewallRule
|
||||
interface FirewallRule {
|
||||
name: string
|
||||
description: string
|
||||
applicationName: string
|
||||
serviceName: string
|
||||
protocol: ProtocolType
|
||||
localPorts: string
|
||||
remotePorts: string
|
||||
localAddresses: string
|
||||
remoteAddresses: string
|
||||
icmpTypesAndCodes: string
|
||||
direction: RuleDirection
|
||||
enabled: boolean
|
||||
profiles: FirewallProfile[]
|
||||
edgeTraversal: boolean
|
||||
action: RuleAction
|
||||
grouping: string
|
||||
interfaceTypes: string
|
||||
interfaces: string[]
|
||||
|
||||
// 高级属性
|
||||
edgeTraversalAllowed: boolean
|
||||
looseSourceMapping: boolean
|
||||
localOnlyMapping: boolean
|
||||
remoteMachineAuthorizationList: string
|
||||
remoteUserAuthorizationList: string
|
||||
embeddedContext: string
|
||||
flags: number
|
||||
secureFlags: boolean
|
||||
}
|
||||
|
||||
// 创建规则请求接口 - 对应后端 CreateRuleRequest
|
||||
interface CreateRuleRequest extends Omit<FirewallRule, 'profiles'> {
|
||||
profiles: number // 使用位标志表示多个配置文件
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
editRule?: FirewallRule | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
save: [rule: CreateRuleRequest]
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const message = useMessage()
|
||||
const formRef = ref<FormInst>()
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!props.editRule)
|
||||
const activeTab = ref('basic')
|
||||
const saving = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const defaultFormData: FirewallRule = {
|
||||
name: '',
|
||||
description: '',
|
||||
applicationName: '',
|
||||
serviceName: '',
|
||||
protocol: ProtocolType.Any,
|
||||
localPorts: '',
|
||||
remotePorts: '',
|
||||
localAddresses: '*',
|
||||
remoteAddresses: '*',
|
||||
icmpTypesAndCodes: '',
|
||||
direction: RuleDirection.Inbound,
|
||||
enabled: true,
|
||||
profiles: [FirewallProfile.Domain, FirewallProfile.Private],
|
||||
edgeTraversal: false,
|
||||
action: RuleAction.Allow,
|
||||
grouping: '',
|
||||
interfaceTypes: 'All',
|
||||
interfaces: [],
|
||||
|
||||
// 高级属性
|
||||
edgeTraversalAllowed: false,
|
||||
looseSourceMapping: false,
|
||||
localOnlyMapping: false,
|
||||
remoteMachineAuthorizationList: '',
|
||||
remoteUserAuthorizationList: '',
|
||||
embeddedContext: '',
|
||||
flags: 0,
|
||||
secureFlags: false,
|
||||
}
|
||||
|
||||
const formData = ref<FirewallRule>({ ...defaultFormData })
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入规则名称', trigger: 'blur' },
|
||||
{ min: 1, max: 255, message: '规则名称长度应在1-255字符之间', trigger: 'blur' }
|
||||
],
|
||||
direction: [
|
||||
{ required: true, message: '请选择规则方向', trigger: 'change', type: 'number' }
|
||||
],
|
||||
action: [
|
||||
{ required: true, message: '请选择规则动作', trigger: 'change', type: 'number' }
|
||||
],
|
||||
protocol: [
|
||||
{ required: true, message: '请选择协议类型', trigger: 'change', type: 'number' }
|
||||
],
|
||||
profiles: [
|
||||
{
|
||||
required: true,
|
||||
message: '请至少选择一个配置文件',
|
||||
trigger: 'change',
|
||||
validator: (rule, value: FirewallProfile[]) => {
|
||||
return value && value.length > 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 选项数据
|
||||
const protocolOptions = [
|
||||
{ label: '任意协议', value: ProtocolType.Any },
|
||||
{ label: 'TCP', value: ProtocolType.TCP },
|
||||
{ label: 'UDP', value: ProtocolType.UDP },
|
||||
{ label: 'ICMPv4', value: ProtocolType.ICMPV4 },
|
||||
{ label: 'ICMPv6', value: ProtocolType.ICMPv6 },
|
||||
{ label: 'IGMP', value: ProtocolType.IGMP },
|
||||
{ label: 'IPv6', value: ProtocolType.IPv6 },
|
||||
{ label: 'GRE', value: ProtocolType.GRE },
|
||||
{ label: 'VRRP', value: ProtocolType.VRRP },
|
||||
{ label: 'PGM', value: ProtocolType.PGM },
|
||||
{ label: 'L2TP', value: ProtocolType.L2TP },
|
||||
]
|
||||
|
||||
const serviceOptions = [
|
||||
{ label: 'Windows Update (wuauserv)', value: 'wuauserv' },
|
||||
{ label: '远程桌面服务 (TermService)', value: 'TermService' },
|
||||
{ label: 'Windows Defender (WinDefend)', value: 'WinDefend' },
|
||||
{ label: '打印后台处理程序 (Spooler)', value: 'Spooler' },
|
||||
{ label: 'Windows 防火墙 (MpsSvc)', value: 'MpsSvc' },
|
||||
{ label: 'DNS 客户端 (Dnscache)', value: 'Dnscache' },
|
||||
{ label: 'DHCP 客户端 (Dhcp)', value: 'Dhcp' },
|
||||
{ label: 'Workstation (LanmanWorkstation)', value: 'LanmanWorkstation' },
|
||||
{ label: 'Server (LanmanServer)', value: 'LanmanServer' },
|
||||
]
|
||||
|
||||
// 监听编辑规则变化
|
||||
watch(() => props.editRule, (newRule) => {
|
||||
if (newRule) {
|
||||
formData.value = { ...newRule }
|
||||
} else {
|
||||
formData.value = { ...defaultFormData }
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 方法
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
setTimeout(() => {
|
||||
formData.value = { ...defaultFormData }
|
||||
activeTab.value = 'basic'
|
||||
formRef.value?.restoreValidation()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const handleValidate = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
message.success('规则验证通过')
|
||||
} catch (errors) {
|
||||
message.error('请检查表单中的错误')
|
||||
console.log(errors)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
saving.value = true
|
||||
|
||||
// 转换数据格式以匹配后端要求
|
||||
const saveData: CreateRuleRequest = {
|
||||
...formData.value,
|
||||
// 将配置文件数组转换为位标志
|
||||
profiles: formData.value.profiles.reduce((acc, profile) => acc | profile, 0)
|
||||
}
|
||||
|
||||
// 模拟保存延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
emit('save', saveData)
|
||||
message.success(isEdit.value ? '规则更新成功' : '规则创建成功')
|
||||
handleClose()
|
||||
} catch (errors) {
|
||||
message.error('请检查表单中的错误')
|
||||
console.log(errors)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const browseApplication = () => {
|
||||
// 这里可以集成文件选择器或调用 Electron API
|
||||
message.info('请使用文件浏览器选择应用程序')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础模态框样式 */
|
||||
:deep(.firewall-rule-modal .n-card) {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--border-primary);
|
||||
box-shadow: var(--shadow-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__header) {
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
padding: 20px 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__content) {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__footer) {
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
padding: 16px 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.rule-form-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.firewall-form {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
.rule-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-nav) {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
padding: 0 24px;
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-pane-wrapper) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-pane) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 标签页内容 */
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
scroll-behavior: smooth;
|
||||
min-height: 0;
|
||||
/* 新增:计算可用高度 */
|
||||
max-height: calc(90vh - 140px * 2); /* 90vh - header - footer - tabs nav */
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb {
|
||||
background: var(--border-secondary);
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 表单区块样式 */
|
||||
.form-section {
|
||||
margin-bottom: 28px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-quaternary);
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title .n-icon {
|
||||
color: var(--accent-primary);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 表单网格 */
|
||||
.form-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 配置文件选项样式 */
|
||||
.profile-option {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.profile-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 模态框底部 */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 标签页切换动画 */
|
||||
:deep(.n-tabs-tab) {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
:deep(.n-tabs-tab:hover) {
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
:deep(.n-tabs-tab--active) {
|
||||
color: var(--accent-primary) !important;
|
||||
background: var(--bg-card);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Naive UI 组件样式覆盖 */
|
||||
:deep(.n-form-item-label) {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.n-form-item-feedback) {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
--n-color: var(--bg-card);
|
||||
--n-text-color: var(--text-secondary);
|
||||
--n-border-radius: 6px;
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
--n-color: var(--bg-card);
|
||||
--n-text-color: var(--text-secondary);
|
||||
--n-border-radius: 6px;
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-checkbox) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-checked: 1px solid var(--accent-primary);
|
||||
--n-color-checked: var(--accent-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-radio) {
|
||||
--n-dot-color-active: var(--accent-primary);
|
||||
--n-color-active: var(--accent-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-checkbox-group) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-radio-group) {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 标签页内容切换动画 */
|
||||
.tab-content {
|
||||
animation: fadeInSlide 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
:deep(.firewall-rule-modal .n-card) {
|
||||
width: 95vw !important;
|
||||
height: 95vh !important;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.n-radio-group) {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-nav) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.tab-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,19 +8,40 @@
|
||||
<p class="view-subtitle">配置和管理网络防护规则</p>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<n-button type="primary" size="medium">
|
||||
<!-- 防火墙总开关 -->
|
||||
<div class="firewall-status">
|
||||
<span class="status-label">防火墙状态</span>
|
||||
<n-switch v-model:value="firewallEnabled" size="medium" />
|
||||
</div>
|
||||
<n-button type="primary" size="medium" @click="openRuleForm()">
|
||||
<template #icon>
|
||||
<n-icon :component="AddOutline" />
|
||||
</template>
|
||||
添加规则
|
||||
新建规则
|
||||
</n-button>
|
||||
<n-button size="medium" ghost>导入规则</n-button>
|
||||
<n-button size="medium" ghost>导出规则</n-button>
|
||||
<n-dropdown :options="batchOptions" placement="bottom-end">
|
||||
<n-button size="medium" ghost>
|
||||
批量操作
|
||||
<template #icon>
|
||||
<n-icon :component="ChevronDownOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规则统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper primary">
|
||||
<n-icon :component="ListOutline" size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.total }}</div>
|
||||
<div class="stat-label">总规则数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper success">
|
||||
<n-icon :component="CheckmarkCircleOutline" size="24" />
|
||||
@@ -47,17 +68,7 @@
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.blocked }}</div>
|
||||
<div class="stat-label">已拦截</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper info">
|
||||
<n-icon :component="ShieldCheckmarkOutline" size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.total }}</div>
|
||||
<div class="stat-label">总规则数</div>
|
||||
<div class="stat-label">阻止连接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,177 +80,399 @@
|
||||
<div class="panel-controls">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索规则..."
|
||||
placeholder="搜索规则名称..."
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon :component="SearchOutline" />
|
||||
</template>
|
||||
</n-input>
|
||||
<n-select
|
||||
v-model:value="filterType"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
:options="filterOptions"
|
||||
/>
|
||||
<n-dropdown :options="filterDropdownOptions" placement="bottom-end">
|
||||
<n-button size="small" ghost>
|
||||
<template #icon>
|
||||
<n-icon :component="FilterOutline" />
|
||||
</template>
|
||||
筛选
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rules-list">
|
||||
<div v-for="rule in mockRules" :key="rule.id" class="rule-item">
|
||||
<div class="rule-status">
|
||||
<n-switch v-model:value="rule.enabled" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="rule-main">
|
||||
<div class="rule-header">
|
||||
<h4 class="rule-name">{{ rule.name }}</h4>
|
||||
<n-tag :type="rule.action === 'allow' ? 'success' : 'error'" size="small">
|
||||
{{ rule.action === 'allow' ? '允许' : '拒绝' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
<div class="rule-details">
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="LocationOutline" size="14" />
|
||||
{{ rule.source }}
|
||||
</span>
|
||||
<span class="rule-arrow">→</span>
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="LocationOutline" size="14" />
|
||||
{{ rule.destination }}
|
||||
</span>
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="LayersOutline" size="14" />
|
||||
{{ rule.protocol }}
|
||||
</span>
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="ServerOutline" size="14" />
|
||||
端口: {{ rule.port }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-actions">
|
||||
<n-button size="tiny" circle quaternary>
|
||||
<template #icon>
|
||||
<n-icon :component="CreateOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="tiny" circle quaternary>
|
||||
<template #icon>
|
||||
<n-icon :component="CopyOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="tiny" circle quaternary type="error">
|
||||
<template #icon>
|
||||
<n-icon :component="TrashOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="filteredRules"
|
||||
:pagination="false"
|
||||
:scroll-x="1200"
|
||||
:virtual-scroll="true"
|
||||
:max-height="600"
|
||||
size="small"
|
||||
striped
|
||||
:row-key="(row) => row.id"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防火墙规则表单 -->
|
||||
<FirewallRuleForm
|
||||
v-model="showRuleForm"
|
||||
:edit-rule="currentEditRule"
|
||||
@save="handleSaveRule"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { NButton, NIcon, NInput, NSelect, NSwitch, NTag } from 'naive-ui'
|
||||
import { ref, computed, h } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NIcon,
|
||||
NInput,
|
||||
NSwitch,
|
||||
NTag,
|
||||
NDataTable,
|
||||
NDropdown,
|
||||
useMessage,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
|
||||
import {
|
||||
AddOutline,
|
||||
SearchOutline,
|
||||
CheckmarkCircleOutline,
|
||||
PauseCircleOutline,
|
||||
CloseCircleOutline,
|
||||
ShieldCheckmarkOutline,
|
||||
LocationOutline,
|
||||
LayersOutline,
|
||||
ServerOutline,
|
||||
ListOutline,
|
||||
ChevronDownOutline,
|
||||
FilterOutline,
|
||||
CreateOutline,
|
||||
CopyOutline,
|
||||
TrashOutline,
|
||||
ArrowUpOutline,
|
||||
ArrowDownOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 界面状态
|
||||
const showRuleForm = ref(false)
|
||||
const currentEditRule = ref(null)
|
||||
const firewallEnabled = ref(true)
|
||||
const checkedRowKeys = ref<string[]>([])
|
||||
|
||||
// 搜索和过滤
|
||||
const searchQuery = ref('')
|
||||
const filterType = ref('all')
|
||||
|
||||
const filterOptions = [
|
||||
{ label: '全部规则', value: 'all' },
|
||||
{ label: '已启用', value: 'active' },
|
||||
{ label: '已禁用', value: 'inactive' },
|
||||
{ label: '入站规则', value: 'inbound' },
|
||||
{ label: '出站规则', value: 'outbound' },
|
||||
// 批量操作选项
|
||||
const batchOptions = [
|
||||
{
|
||||
label: '启用选中',
|
||||
key: 'enable',
|
||||
icon: () => h(NIcon, { component: CheckmarkCircleOutline })
|
||||
},
|
||||
{
|
||||
label: '禁用选中',
|
||||
key: 'disable',
|
||||
icon: () => h(NIcon, { component: PauseCircleOutline })
|
||||
},
|
||||
{
|
||||
label: '删除选中',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, { component: TrashOutline })
|
||||
}
|
||||
]
|
||||
|
||||
// 筛选下拉选项
|
||||
const filterDropdownOptions = [
|
||||
{
|
||||
label: '方向',
|
||||
key: 'direction-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '入站规则', key: 'inbound' },
|
||||
{ label: '出站规则', key: 'outbound' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '操作',
|
||||
key: 'action-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '允许', key: 'allow' },
|
||||
{ label: '阻止', key: 'block' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
key: 'status-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '已启用', key: 'enabled' },
|
||||
{ label: '已禁用', key: 'disabled' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
active: 128,
|
||||
inactive: 24,
|
||||
blocked: 3456,
|
||||
total: 152,
|
||||
total: 127,
|
||||
active: 89,
|
||||
inactive: 38,
|
||||
blocked: 1247,
|
||||
})
|
||||
|
||||
// 模拟规则数据
|
||||
const mockRules = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '阻止恶意IP访问',
|
||||
id: '1',
|
||||
name: 'Windows 远程桌面',
|
||||
description: '允许远程桌面连接',
|
||||
enabled: true,
|
||||
action: 'deny',
|
||||
source: '10.0.0.0/8',
|
||||
destination: '任意',
|
||||
protocol: 'TCP/UDP',
|
||||
port: '全部',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
program: '系统服务',
|
||||
protocol: 'TCP',
|
||||
port: '3389',
|
||||
profiles: ['域', '专用'],
|
||||
priority: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '允许HTTPS流量',
|
||||
id: '2',
|
||||
name: 'Microsoft Edge',
|
||||
description: '允许浏览器访问网络',
|
||||
enabled: true,
|
||||
direction: 'outbound',
|
||||
action: 'allow',
|
||||
source: '任意',
|
||||
destination: '任意',
|
||||
protocol: 'TCP',
|
||||
port: '443',
|
||||
program: 'C:\\Program Files\\Microsoft\\Edge\\msedge.exe',
|
||||
protocol: '任意',
|
||||
port: '任意',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 200,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '允许DNS查询',
|
||||
id: '3',
|
||||
name: '阻止 Telnet',
|
||||
description: '安全策略:阻止 Telnet 连接',
|
||||
enabled: true,
|
||||
action: 'allow',
|
||||
source: '192.168.1.0/24',
|
||||
destination: '8.8.8.8',
|
||||
protocol: 'UDP',
|
||||
port: '53',
|
||||
direction: 'inbound',
|
||||
action: 'block',
|
||||
program: '任意',
|
||||
protocol: 'TCP',
|
||||
port: '23',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 300,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '阻止Telnet访问',
|
||||
id: '4',
|
||||
name: '文件和打印机共享',
|
||||
description: '允许局域网内文件共享',
|
||||
enabled: false,
|
||||
action: 'deny',
|
||||
source: '任意',
|
||||
destination: '任意',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
program: '系统服务',
|
||||
protocol: 'TCP',
|
||||
port: '23',
|
||||
port: '445, 139',
|
||||
profiles: ['专用'],
|
||||
priority: 400,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '允许SSH管理',
|
||||
id: '5',
|
||||
name: 'HTTP 服务器',
|
||||
description: '允许 Web 服务器接收请求',
|
||||
enabled: true,
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
source: '192.168.1.100',
|
||||
destination: '192.168.1.1',
|
||||
program: '任意',
|
||||
protocol: 'TCP',
|
||||
port: '22',
|
||||
port: '80, 443',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 500,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns: DataTableColumns = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 40
|
||||
},
|
||||
{
|
||||
title: '规则名称',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'rule-name-cell' }, [
|
||||
h('div', { class: 'rule-name' }, row.name),
|
||||
h('div', { class: 'rule-description' }, row.description)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enabled',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h(NTag, {
|
||||
type: row.enabled ? 'success' : 'default',
|
||||
size: 'small'
|
||||
}, {
|
||||
default: () => row.enabled ? '启用' : '禁用'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
key: 'direction',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'direction-cell' }, [
|
||||
h(NIcon, {
|
||||
component: row.direction === 'inbound' ? ArrowDownOutline : ArrowUpOutline,
|
||||
size: 16,
|
||||
style: { marginRight: '4px' }
|
||||
}),
|
||||
row.direction === 'inbound' ? '入站' : '出站'
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h(NTag, {
|
||||
type: row.action === 'allow' ? 'success' : 'error',
|
||||
size: 'small'
|
||||
}, {
|
||||
default: () => row.action === 'allow' ? '允许' : '阻止'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '程序/服务',
|
||||
key: 'program',
|
||||
width: 250,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '协议',
|
||||
key: 'protocol',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '配置文件',
|
||||
key: 'profiles',
|
||||
width: 150,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'profiles-cell' },
|
||||
row.profiles.map((profile: string) =>
|
||||
h(NTag, {
|
||||
key: profile,
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
style: { marginRight: '4px', marginBottom: '2px' }
|
||||
}, {
|
||||
default: () => profile
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'action-buttons' }, [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
onClick: () => editRule(row)
|
||||
}, {
|
||||
default: () => '编辑'
|
||||
}),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
type: 'error',
|
||||
onClick: () => deleteRule(row.id)
|
||||
}, {
|
||||
default: () => '删除'
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredRules = computed(() => {
|
||||
let rules = mockRules.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
rules = rules.filter(rule =>
|
||||
rule.name.toLowerCase().includes(query) ||
|
||||
rule.description.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return rules
|
||||
})
|
||||
|
||||
// 方法
|
||||
const openRuleForm = (rule: any = null) => {
|
||||
currentEditRule.value = rule
|
||||
showRuleForm.value = true
|
||||
}
|
||||
|
||||
const editRule = (rule: any) => {
|
||||
currentEditRule.value = { ...rule }
|
||||
showRuleForm.value = true
|
||||
}
|
||||
|
||||
const deleteRule = (id: string) => {
|
||||
const index = mockRules.value.findIndex(rule => rule.id === id)
|
||||
if (index !== -1) {
|
||||
mockRules.value.splice(index, 1)
|
||||
message.success('规则删除成功')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheck = (keys: string[]) => {
|
||||
checkedRowKeys.value = keys
|
||||
}
|
||||
|
||||
const handleSaveRule = (rule: any) => {
|
||||
if (rule.id) {
|
||||
// 编辑现有规则
|
||||
const index = mockRules.value.findIndex(r => r.id === rule.id)
|
||||
if (index !== -1) {
|
||||
mockRules.value[index] = rule
|
||||
message.success('规则更新成功')
|
||||
}
|
||||
} else {
|
||||
// 添加新规则
|
||||
rule.id = Date.now().toString()
|
||||
mockRules.value.unshift(rule)
|
||||
message.success('规则创建成功')
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
currentEditRule.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -261,6 +494,7 @@ const mockRules = ref([
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
@@ -282,9 +516,26 @@ const mockRules = ref([
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.firewall-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
@@ -319,6 +570,11 @@ const mockRules = ref([
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.primary {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--accent-success);
|
||||
@@ -334,11 +590,6 @@ const mockRules = ref([
|
||||
color: var(--accent-error);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -381,120 +632,132 @@ const mockRules = ref([
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 规则列表 */
|
||||
.rules-list {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.rule-item:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.rule-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rule-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rule-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.rule-details {
|
||||
/* 表格单元格样式 */
|
||||
.rule-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.rule-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rule-arrow {
|
||||
color: var(--text-quaternary);
|
||||
.rule-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
.rule-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.direction-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.profiles-cell {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1200px) {
|
||||
.firewall-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.firewall-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
.toolbar-right {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule-details {
|
||||
flex-wrap: wrap;
|
||||
.firewall-status {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* Naive UI 样式覆盖 */
|
||||
/* Naive UI 数据表格样式覆盖 */
|
||||
:deep(.n-data-table) {
|
||||
--n-td-color: var(--bg-card);
|
||||
--n-td-color-striped: var(--bg-tertiary);
|
||||
--n-th-color: var(--bg-secondary);
|
||||
--n-border-color: var(--border-secondary);
|
||||
--n-th-text-color: var(--text-secondary);
|
||||
--n-td-text-color: var(--text-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-th) {
|
||||
font-weight: 600;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
:deep(.n-data-table-td) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-th) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
@@ -503,12 +766,9 @@ const mockRules = ref([
|
||||
--n-text-color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
:deep(.n-dropdown-menu) {
|
||||
--n-color: var(--bg-card);
|
||||
--n-text-color: var(--text-primary);
|
||||
--n-option-text-color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-switch) {
|
||||
@@ -517,5 +777,6 @@ const mockRules = ref([
|
||||
|
||||
:deep(.n-tag) {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user