U:Vue布局实现

This commit is contained in:
jimmy committed 2025-07-25 11:16:04 +08:00
1 parent 60d6941074
commit c55bf3c9ca
4 files changed
+1422 -200

No files matched your search

+3
View File
@@ -1,2 +1,5 @@
VITE_MODE='development'
VITE_APP_API_URL='http://localhost:8268/api'
我想让FirewallRuleForm.vue的滚动区域,只在页面主容器内进行,比如“基础内容”很多,那么就在“基础内容”的
子Tab中进行滚动,不能超出去,不然滚动时将会整体滚动,非常不优雅,而且你这次优化的样式我觉得还有提升空间,你可以参考MonitorView下的组件进行样式优化
+3 -1
View File
@@ -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>
+460 -199
View File
@@ -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>