1
This commit is contained in:
1 parent
e265261fa0
commit
0ab7a2bca4
3 files changed
+250
-579
No files matched your search
@@ -94,7 +94,7 @@ class FirewallApiService {
|
|||||||
direction = rule.direction === RuleDirection.Inbound ? 'inbound' : 'outbound'
|
direction = rule.direction === RuleDirection.Inbound ? 'inbound' : 'outbound'
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: rule.name, // 使用规则名称作为ID
|
id: rule.uniqueId || rule.name, // 优先使用后端提供的uniqueId,降级为name
|
||||||
name: rule.name,
|
name: rule.name,
|
||||||
description: rule.description,
|
description: rule.description,
|
||||||
enabled: rule.enabled,
|
enabled: rule.enabled,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export enum ProtocolType {
|
|||||||
// 防火墙规则详细信息
|
// 防火墙规则详细信息
|
||||||
export interface FirewallRule {
|
export interface FirewallRule {
|
||||||
name: string
|
name: string
|
||||||
|
uniqueId?: string // 添加唯一标识符字段,用于解决重复规则显示问题
|
||||||
description: string
|
description: string
|
||||||
applicationName: string
|
applicationName: string
|
||||||
serviceName: string
|
serviceName: string
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-number">{{ stats.total }}</div>
|
<div class="stat-number">{{ stats.total }}</div>
|
||||||
<div class="stat-label">总规则数</div>
|
<div class="stat-label">禁用规则</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,29 +50,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-number">{{ stats.inactive }}</div>
|
<div class="stat-number">{{ stats.inactive }}</div>
|
||||||
<div class="stat-label">禁用规则</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon-wrapper info">
|
|
||||||
<n-icon :component="ArrowDownOutline" size="24" />
|
|
||||||
</div>
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-number">{{ stats.inbound }}</div>
|
|
||||||
<div class="stat-label">入站规则</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon-wrapper info">
|
|
||||||
<n-icon :component="ArrowUpOutline" size="24" />
|
|
||||||
</div>
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-number">{{ stats.outbound }}</div>
|
|
||||||
<div class="stat-label">出站规则</div>
|
<div class="stat-label">出站规则</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon-wrapper error">
|
||||||
|
<n-icon :component="CloseCircleOutline" size="24" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.blocked }}</div>
|
||||||
|
<div class="stat-label">入站规则</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 规则列表 -->
|
<!-- 规则列表 -->
|
||||||
@@ -80,20 +70,6 @@
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3 class="panel-title">防火墙规则列表</h3>
|
<h3 class="panel-title">防火墙规则列表</h3>
|
||||||
<div class="panel-controls">
|
<div class="panel-controls">
|
||||||
<!-- 批量操作按钮 -->
|
|
||||||
<n-dropdown
|
|
||||||
v-if="persistentSelectedRules.size > 0"
|
|
||||||
:options="batchOptions"
|
|
||||||
@select="handleBatchOperation"
|
|
||||||
>
|
|
||||||
<n-button size="small" type="primary">
|
|
||||||
批量操作 ({{ persistentSelectedRules.size }})
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="ChevronDownOutline" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</n-dropdown>
|
|
||||||
|
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="searchQuery"
|
v-model:value="searchQuery"
|
||||||
placeholder="搜索规则名称..."
|
placeholder="搜索规则名称..."
|
||||||
@@ -185,50 +161,91 @@
|
|||||||
<div class="header-cell actions-cell">操作</div>
|
<div class="header-cell actions-cell">操作</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading && totalCount === 0" class="loading-container">
|
|
||||||
<n-spin size="large">
|
|
||||||
<div class="loading-text">正在加载防火墙规则...</div>
|
|
||||||
</n-spin>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 虚拟滚动列表 -->
|
<!-- 虚拟滚动列表 -->
|
||||||
<RecycleScroller
|
<RecycleScroller
|
||||||
v-else
|
|
||||||
class="rules-scroller"
|
class="rules-scroller"
|
||||||
:items="filteredRules"
|
:items="filteredRules"
|
||||||
:item-size="70"
|
:item-size="80"
|
||||||
key-field="id"
|
key-field="id"
|
||||||
v-slot="{ item }"
|
v-slot="{ item }"
|
||||||
@scroll="handleScroll"
|
|
||||||
>
|
>
|
||||||
<RuleItem
|
<div class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
|
||||||
:virtual-item="item"
|
<div class="rule-cell checkbox-cell">
|
||||||
:get-rule="getRuleForItem"
|
<n-checkbox
|
||||||
:load-rules-in-range="loadRulesInRange"
|
:checked="checkedRowKeys.includes(item.id)"
|
||||||
@check="handleRuleCheck"
|
@update:checked="(checked) => handleRuleCheck(item.id, checked)"
|
||||||
@edit="editRule"
|
/>
|
||||||
@delete="deleteRule"
|
</div>
|
||||||
:checked="isRuleSelected(item.id)"
|
|
||||||
/>
|
<div class="rule-cell name-cell">
|
||||||
|
<div class="rule-name">{{ item.name }}</div>
|
||||||
|
<div class="rule-description">{{ item.description }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell status-cell">
|
||||||
|
<n-tag :type="item.enabled ? 'success' : 'default'" size="small">
|
||||||
|
{{ item.enabled ? '启用' : '禁用' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell direction-cell">
|
||||||
|
<div class="direction-content">
|
||||||
|
<n-icon
|
||||||
|
:component="item.direction === 'inbound' ? ArrowDownOutline : ArrowUpOutline"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
<span>{{ item.direction === 'inbound' ? '入站' : '出站' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell action-cell">
|
||||||
|
<n-tag :type="item.action === 'allow' ? 'success' : 'error'" size="small">
|
||||||
|
{{ item.action === 'allow' ? '允许' : '阻止' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell program-cell">
|
||||||
|
<div class="program-text" :title="item.program">{{ item.program }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell protocol-cell">
|
||||||
|
<span>{{ item.protocol }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell port-cell">
|
||||||
|
<span>{{ item.port }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell profiles-cell">
|
||||||
|
<div class="profiles-container">
|
||||||
|
<n-tag
|
||||||
|
v-for="profile in item.profiles"
|
||||||
|
:key="profile"
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
class="profile-tag"
|
||||||
|
>
|
||||||
|
{{ profile }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rule-cell actions-cell">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<n-button size="small" quaternary @click="editRule(item)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CreateOutline" size="14" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" quaternary type="error" @click="deleteRule(item.id)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="TrashOutline" size="14" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</RecycleScroller>
|
</RecycleScroller>
|
||||||
|
|
||||||
<!-- 底部状态 -->
|
|
||||||
<div v-if="totalCount > 0" class="list-footer">
|
|
||||||
<span>共 {{ totalCount }} 条规则</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<div v-if="!loading && totalCount === 0" class="empty-state">
|
|
||||||
<n-icon :component="ListOutline" size="48" />
|
|
||||||
<div class="empty-text">暂无防火墙规则</div>
|
|
||||||
<n-button type="primary" @click="openRuleForm()">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="AddOutline" />
|
|
||||||
</template>
|
|
||||||
创建第一个规则
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +256,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, h, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, h } from 'vue'
|
||||||
import {
|
import {
|
||||||
NButton,
|
NButton,
|
||||||
NIcon,
|
NIcon,
|
||||||
@@ -250,11 +267,9 @@ import {
|
|||||||
NCheckbox,
|
NCheckbox,
|
||||||
NSelect,
|
NSelect,
|
||||||
useMessage,
|
useMessage,
|
||||||
NSpin,
|
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||||
import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
|
import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
|
||||||
import RuleItem from '@/components/RuleItem.vue'
|
|
||||||
import {
|
import {
|
||||||
AddOutline,
|
AddOutline,
|
||||||
SearchOutline,
|
SearchOutline,
|
||||||
@@ -269,27 +284,14 @@ import {
|
|||||||
ArrowDownOutline,
|
ArrowDownOutline,
|
||||||
RefreshOutline,
|
RefreshOutline,
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { firewallApi } from '@/api/firewall'
|
|
||||||
import type { DisplayRule, RuleFilter, FirewallStatistics } from '@/types/firewall'
|
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
// 界面状态
|
// 界面状态
|
||||||
const showRuleForm = ref(false)
|
const showRuleForm = ref(false)
|
||||||
const currentEditRule = ref<DisplayRule | null>(null)
|
const currentEditRule = ref(null)
|
||||||
const firewallEnabled = ref(true)
|
const firewallEnabled = ref(true)
|
||||||
const checkedRowKeys = ref<string[]>([])
|
const checkedRowKeys = ref<string[]>([])
|
||||||
const persistentSelectedRules = ref<Set<string>>(new Set()) // 持久化选中的规则ID
|
|
||||||
const loading = ref(false)
|
|
||||||
const totalCount = ref(0)
|
|
||||||
|
|
||||||
// 虚拟滚动项类型
|
|
||||||
interface VirtualItem {
|
|
||||||
id: string
|
|
||||||
index: number
|
|
||||||
rule?: DisplayRule
|
|
||||||
loading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索和过滤
|
// 搜索和过滤
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
@@ -300,20 +302,6 @@ const filters = ref({
|
|||||||
protocol: null as string | null,
|
protocol: null as string | null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分页状态
|
|
||||||
const pagination = ref({
|
|
||||||
start: 0,
|
|
||||||
limit: 50,
|
|
||||||
totalCount: 0,
|
|
||||||
hasMore: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 数据状态
|
|
||||||
const rules = ref<DisplayRule[]>([])
|
|
||||||
const virtualItems = ref<VirtualItem[]>([]) // 虚拟滚动项
|
|
||||||
const loadedRules = ref<Map<number, DisplayRule>>(new Map()) // 缓存已加载的规则
|
|
||||||
const loadingRanges = ref<Set<string>>(new Set()) // 正在加载的范围,防止重复请求
|
|
||||||
|
|
||||||
// 过滤选项
|
// 过滤选项
|
||||||
const directionOptions = [
|
const directionOptions = [
|
||||||
{ label: '入站', value: 'inbound' },
|
{ label: '入站', value: 'inbound' },
|
||||||
@@ -357,329 +345,179 @@ const batchOptions = [
|
|||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
const stats = ref({
|
const stats = ref({
|
||||||
total: 0,
|
total: 127,
|
||||||
active: 0,
|
active: 89,
|
||||||
inactive: 0,
|
inactive: 38,
|
||||||
inbound: 0,
|
blocked: 1247,
|
||||||
outbound: 0,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// API方法
|
// 模拟规则数据
|
||||||
/**
|
const mockRules = ref([
|
||||||
* 获取防火墙规则总数
|
{
|
||||||
*/
|
id: '1',
|
||||||
const getTotalCount = async () => {
|
name: 'Windows 远程桌面',
|
||||||
try {
|
description: '允许远程桌面连接',
|
||||||
// 构建筛选条件(不包含分页参数)
|
enabled: true,
|
||||||
const filter: RuleFilter = {}
|
direction: 'inbound',
|
||||||
|
action: 'allow',
|
||||||
// 搜索条件
|
program: '系统服务',
|
||||||
if (searchQuery.value.trim()) {
|
protocol: 'TCP',
|
||||||
filter.name = searchQuery.value.trim()
|
port: '3389',
|
||||||
}
|
profiles: ['域', '专用'],
|
||||||
|
priority: 100,
|
||||||
// 筛选条件
|
},
|
||||||
if (filters.value.direction) {
|
{
|
||||||
filter.direction = filters.value.direction
|
id: '2',
|
||||||
}
|
name: 'Microsoft Edge',
|
||||||
if (filters.value.action) {
|
description: '允许浏览器访问网络',
|
||||||
filter.action = filters.value.action
|
enabled: true,
|
||||||
}
|
direction: 'outbound',
|
||||||
if (filters.value.enabled !== null) {
|
action: 'allow',
|
||||||
filter.enabled = filters.value.enabled
|
program: 'C:\\Program Files\\Microsoft\\Edge\\msedge.exe',
|
||||||
}
|
protocol: '任意',
|
||||||
if (filters.value.protocol) {
|
port: '任意',
|
||||||
filter.protocol = filters.value.protocol
|
profiles: ['域', '专用', '公用'],
|
||||||
}
|
priority: 200,
|
||||||
|
},
|
||||||
// 只获取第一页来得到总数
|
{
|
||||||
filter.start = 0
|
id: '3',
|
||||||
filter.limit = 1
|
name: '阻止 Telnet',
|
||||||
|
description: '安全策略:阻止 Telnet 连接',
|
||||||
const response = await firewallApi.getRules(filter)
|
enabled: true,
|
||||||
totalCount.value = response.totalCount
|
direction: 'inbound',
|
||||||
|
action: 'block',
|
||||||
// 创建虚拟项
|
program: '任意',
|
||||||
createVirtualItems()
|
protocol: 'TCP',
|
||||||
|
port: '23',
|
||||||
} catch (error) {
|
profiles: ['域', '专用', '公用'],
|
||||||
console.error('获取防火墙规则总数失败:', error)
|
priority: 300,
|
||||||
message.error('获取防火墙规则总数失败')
|
},
|
||||||
}
|
{
|
||||||
}
|
id: '4',
|
||||||
|
name: '文件和打印机共享',
|
||||||
/**
|
description: '允许局域网内文件共享',
|
||||||
* 创建虚拟滚动项
|
enabled: false,
|
||||||
*/
|
direction: 'inbound',
|
||||||
const createVirtualItems = () => {
|
action: 'allow',
|
||||||
virtualItems.value = Array.from({ length: totalCount.value }, (_, index) => ({
|
program: '系统服务',
|
||||||
id: `rule-${index}`,
|
protocol: 'TCP',
|
||||||
index: index
|
port: '445, 139',
|
||||||
}))
|
profiles: ['专用'],
|
||||||
loadedRules.value.clear()
|
priority: 400,
|
||||||
|
},
|
||||||
// 根据持久化选中状态重新构建当前页面的选中状态
|
{
|
||||||
// 注意:这里不直接清空 checkedRowKeys,而是在实际渲染时动态检查
|
id: '5',
|
||||||
// 延迟执行以确保规则已加载
|
name: 'HTTP 服务器',
|
||||||
nextTick(() => {
|
description: '允许 Web 服务器接收请求',
|
||||||
updateCurrentPageSelection()
|
enabled: true,
|
||||||
})
|
direction: 'inbound',
|
||||||
}
|
action: 'allow',
|
||||||
|
program: '任意',
|
||||||
/**
|
protocol: 'TCP',
|
||||||
* 按需加载指定索引范围的规则
|
port: '80, 443',
|
||||||
*/
|
profiles: ['域', '专用', '公用'],
|
||||||
const loadRulesInRange = async (startIndex: number, endIndex: number) => {
|
priority: 500,
|
||||||
// 创建范围标识符,防止重复请求
|
},
|
||||||
const rangeKey = `${startIndex}-${endIndex}`
|
// 添加更多模拟数据以测试虚拟滚动
|
||||||
if (loadingRanges.value.has(rangeKey)) {
|
...Array.from({ length: 50 }, (_, i) => ({
|
||||||
return // 已在加载中,直接返回
|
id: `${i + 6}`,
|
||||||
}
|
name: `规则 ${i + 6}`,
|
||||||
|
description: `这是第 ${i + 6} 个测试规则`,
|
||||||
const needToLoad: number[] = []
|
enabled: Math.random() > 0.5,
|
||||||
|
direction: Math.random() > 0.5 ? 'inbound' : 'outbound',
|
||||||
// 找出需要加载的索引
|
action: Math.random() > 0.5 ? 'allow' : 'block',
|
||||||
for (let i = startIndex; i <= endIndex; i++) {
|
program: `程序 ${i + 6}`,
|
||||||
if (!loadedRules.value.has(i)) {
|
protocol: ['TCP', 'UDP', '任意'][Math.floor(Math.random() * 3)],
|
||||||
needToLoad.push(i)
|
port: Math.floor(Math.random() * 65535).toString(),
|
||||||
}
|
profiles: ['域', '专用', '公用'].slice(0, Math.floor(Math.random() * 3) + 1),
|
||||||
}
|
priority: (i + 6) * 100,
|
||||||
|
})),
|
||||||
if (needToLoad.length === 0) return
|
])
|
||||||
|
|
||||||
// 标记为加载中
|
|
||||||
loadingRanges.value.add(rangeKey)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 优化:将连续的索引合并为一个请求
|
|
||||||
const continuousRanges = getContinuousRanges(needToLoad)
|
|
||||||
|
|
||||||
const requests = continuousRanges.map(async (range) => {
|
|
||||||
// 构建筛选条件
|
|
||||||
const filter: RuleFilter = {
|
|
||||||
start: range.start,
|
|
||||||
limit: range.end - range.start + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用搜索和筛选条件
|
|
||||||
if (searchQuery.value.trim()) {
|
|
||||||
filter.name = searchQuery.value.trim()
|
|
||||||
}
|
|
||||||
if (filters.value.direction) {
|
|
||||||
filter.direction = filters.value.direction
|
|
||||||
}
|
|
||||||
if (filters.value.action) {
|
|
||||||
filter.action = filters.value.action
|
|
||||||
}
|
|
||||||
if (filters.value.enabled !== null) {
|
|
||||||
filter.enabled = filters.value.enabled
|
|
||||||
}
|
|
||||||
if (filters.value.protocol) {
|
|
||||||
filter.protocol = filters.value.protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await firewallApi.getRules(filter)
|
|
||||||
const displayRules = response.rules.map(rule => firewallApi.convertToDisplayRule(rule))
|
|
||||||
|
|
||||||
// 缓存规则数据
|
|
||||||
displayRules.forEach((rule, index) => {
|
|
||||||
const actualIndex = range.start + index
|
|
||||||
loadedRules.value.set(actualIndex, rule)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 等待所有请求完成
|
|
||||||
await Promise.all(requests)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载防火墙规则失败:', error)
|
|
||||||
// 只有非取消的错误才显示给用户
|
|
||||||
if (error.name !== 'AbortError' && !error.message?.includes('Cancel')) {
|
|
||||||
message.error('加载防火墙规则失败')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// 移除加载标记
|
|
||||||
loadingRanges.value.delete(rangeKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将离散的索引数组合并为连续范围
|
|
||||||
*/
|
|
||||||
const getContinuousRanges = (indices: number[]) => {
|
|
||||||
if (indices.length === 0) return []
|
|
||||||
|
|
||||||
indices.sort((a, b) => a - b)
|
|
||||||
const ranges: { start: number; end: number }[] = []
|
|
||||||
let currentStart = indices[0]
|
|
||||||
let currentEnd = indices[0]
|
|
||||||
|
|
||||||
for (let i = 1; i < indices.length; i++) {
|
|
||||||
if (indices[i] === currentEnd + 1) {
|
|
||||||
// 连续的索引
|
|
||||||
currentEnd = indices[i]
|
|
||||||
} else {
|
|
||||||
// 不连续,保存当前范围
|
|
||||||
ranges.push({ start: currentStart, end: currentEnd })
|
|
||||||
currentStart = indices[i]
|
|
||||||
currentEnd = indices[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加最后一个范围
|
|
||||||
ranges.push({ start: currentStart, end: currentEnd })
|
|
||||||
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始加载第一屏数据(仅在初始化时调用)
|
|
||||||
*/
|
|
||||||
const loadInitialData = async () => {
|
|
||||||
// 只加载第一屏的数据,大约20条
|
|
||||||
const initialBatchSize = 20
|
|
||||||
if (totalCount.value > 0) {
|
|
||||||
await loadRulesInRange(0, Math.min(initialBatchSize - 1, totalCount.value - 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定索引的规则数据
|
|
||||||
*/
|
|
||||||
const getRuleByIndex = (index: number): DisplayRule | null => {
|
|
||||||
return loadedRules.value.get(index) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载统计信息
|
|
||||||
*/
|
|
||||||
const loadStatistics = async () => {
|
|
||||||
try {
|
|
||||||
const statistics = await firewallApi.getStatistics()
|
|
||||||
stats.value = {
|
|
||||||
total: statistics.totalRules,
|
|
||||||
active: statistics.enabledRules,
|
|
||||||
inactive: statistics.disabledRules,
|
|
||||||
inbound: statistics.rulesByDirection?.['1'] || 0, // Inbound = 1
|
|
||||||
outbound: statistics.rulesByDirection?.['2'] || 0 // Outbound = 2
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载统计信息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新数据
|
|
||||||
*/
|
|
||||||
const refreshData = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
// 清空缓存和加载标记
|
|
||||||
loadedRules.value.clear()
|
|
||||||
loadingRanges.value.clear()
|
|
||||||
|
|
||||||
await getTotalCount()
|
|
||||||
await loadStatistics()
|
|
||||||
|
|
||||||
// 初始加载第一屏数据
|
|
||||||
await loadInitialData()
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const filteredRules = computed(() => {
|
const filteredRules = computed(() => {
|
||||||
return virtualItems.value
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方向过滤
|
||||||
|
if (filters.value.direction) {
|
||||||
|
rules = rules.filter((rule) => rule.direction === filters.value.direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作过滤
|
||||||
|
if (filters.value.action) {
|
||||||
|
rules = rules.filter((rule) => rule.action === filters.value.action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态过滤
|
||||||
|
if (filters.value.enabled !== null) {
|
||||||
|
rules = rules.filter((rule) => rule.enabled === filters.value.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 协议过滤
|
||||||
|
if (filters.value.protocol) {
|
||||||
|
rules = rules.filter((rule) => rule.protocol === filters.value.protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
})
|
})
|
||||||
|
|
||||||
// 全选相关计算属性
|
// 全选相关计算属性
|
||||||
const allSelected = computed({
|
const allSelected = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return totalCount.value > 0 && persistentSelectedRules.value.size === totalCount.value
|
return (
|
||||||
|
filteredRules.value.length > 0 && checkedRowKeys.value.length === filteredRules.value.length
|
||||||
|
)
|
||||||
},
|
},
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
// 全选:需要加载所有规则才能获取所有规则名称
|
checkedRowKeys.value = filteredRules.value.map((rule) => rule.id)
|
||||||
// 这里简化处理,实际应用中可能需要更复杂的逻辑
|
|
||||||
message.warning('全选功能需要先加载所有规则数据')
|
|
||||||
} else {
|
} else {
|
||||||
// 取消全选
|
|
||||||
persistentSelectedRules.value.clear()
|
|
||||||
checkedRowKeys.value = []
|
checkedRowKeys.value = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const indeterminate = computed(() => {
|
const indeterminate = computed(() => {
|
||||||
return persistentSelectedRules.value.size > 0 && persistentSelectedRules.value.size < totalCount.value
|
return checkedRowKeys.value.length > 0 && checkedRowKeys.value.length < filteredRules.value.length
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
/**
|
const openRuleForm = (rule: unknown = null) => {
|
||||||
* 获取虚拟项的实际规则数据
|
currentEditRule.value = rule
|
||||||
*/
|
showRuleForm.value = true
|
||||||
const getItemRule = async (item: VirtualItem): Promise<DisplayRule | null> => {
|
}
|
||||||
const cachedRule = getRuleByIndex(item.index)
|
|
||||||
if (cachedRule) {
|
const editRule = (rule: unknown) => {
|
||||||
return cachedRule
|
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)
|
||||||
|
// 从选中项中移除
|
||||||
|
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
|
||||||
|
message.success('规则删除成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预加载周围的数据(批量加载提高效率)
|
|
||||||
const batchSize = 20
|
|
||||||
const startIndex = Math.max(0, item.index - batchSize / 2)
|
|
||||||
const endIndex = Math.min(totalCount.value - 1, item.index + batchSize / 2)
|
|
||||||
|
|
||||||
await loadRulesInRange(startIndex, endIndex)
|
|
||||||
|
|
||||||
return getRuleByIndex(item.index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查规则是否被选中(基于规则名称)
|
const handleRuleCheck = (id: string, checked: boolean) => {
|
||||||
const isRuleSelected = (itemId: string): boolean => {
|
|
||||||
const itemIndex = parseInt(itemId.replace('rule-', ''))
|
|
||||||
const rule = getRuleByIndex(itemIndex)
|
|
||||||
return rule ? persistentSelectedRules.value.has(rule.name) : false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同步更新 checkedRowKeys 以反映当前筛选结果中选中的项
|
|
||||||
const updateCurrentPageSelection = () => {
|
|
||||||
const currentPageSelected: string[] = []
|
|
||||||
|
|
||||||
// 遍历当前虚拟项,检查是否在持久化选中列表中
|
|
||||||
virtualItems.value.forEach((item) => {
|
|
||||||
if (isRuleSelected(item.id)) {
|
|
||||||
currentPageSelected.push(item.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
checkedRowKeys.value = currentPageSelected
|
|
||||||
}
|
|
||||||
const getRuleForItem = (index: number): DisplayRule | null => {
|
|
||||||
return getRuleByIndex(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRuleCheck = (itemId: string, checked: boolean) => {
|
|
||||||
// 从虚拟项ID获取对应的规则
|
|
||||||
const itemIndex = parseInt(itemId.replace('rule-', ''))
|
|
||||||
const rule = getRuleByIndex(itemIndex)
|
|
||||||
|
|
||||||
if (!rule) return
|
|
||||||
|
|
||||||
const ruleId = rule.name // 使用规则名称作为唯一ID
|
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
// 添加到持久化选中集合
|
if (!checkedRowKeys.value.includes(id)) {
|
||||||
persistentSelectedRules.value.add(ruleId)
|
checkedRowKeys.value.push(id)
|
||||||
// 添加到当前页面选中列表(如果不存在)
|
|
||||||
if (!checkedRowKeys.value.includes(itemId)) {
|
|
||||||
checkedRowKeys.value.push(itemId)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 从持久化选中集合移除
|
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
|
||||||
persistentSelectedRules.value.delete(ruleId)
|
|
||||||
// 从当前页面选中列表移除
|
|
||||||
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== itemId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,128 +525,33 @@ const handleSelectAll = (checked: boolean) => {
|
|||||||
allSelected.value = checked
|
allSelected.value = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearFilters = async () => {
|
const clearFilters = () => {
|
||||||
filters.value = {
|
filters.value = {
|
||||||
direction: null,
|
direction: null,
|
||||||
action: null,
|
action: null,
|
||||||
enabled: null,
|
enabled: null,
|
||||||
protocol: null,
|
protocol: null,
|
||||||
}
|
}
|
||||||
searchQuery.value = ''
|
|
||||||
await refreshData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理规则编辑
|
const handleSaveRule = (rule: unknown) => {
|
||||||
const editRule = (rule: DisplayRule) => {
|
if (rule.id) {
|
||||||
currentEditRule.value = { ...rule }
|
// 编辑现有规则
|
||||||
showRuleForm.value = true
|
const index = mockRules.value.findIndex((r) => r.id === rule.id)
|
||||||
}
|
if (index !== -1) {
|
||||||
|
mockRules.value[index] = rule
|
||||||
const openRuleForm = (rule: DisplayRule | null = null) => {
|
|
||||||
currentEditRule.value = rule
|
|
||||||
showRuleForm.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveRule = async (rule: DisplayRule) => {
|
|
||||||
try {
|
|
||||||
if (currentEditRule.value?.name) {
|
|
||||||
// 编辑现有规则
|
|
||||||
const updateRequest = firewallApi.convertToCreateRequest(rule)
|
|
||||||
await firewallApi.updateRule({
|
|
||||||
currentName: currentEditRule.value.name,
|
|
||||||
newName: rule.name !== currentEditRule.value.name ? rule.name : undefined,
|
|
||||||
description: rule.description,
|
|
||||||
enabled: rule.enabled,
|
|
||||||
action: rule.action === 'allow' ? 1 : 0,
|
|
||||||
// 其他需要的字段...
|
|
||||||
})
|
|
||||||
message.success('规则更新成功')
|
message.success('规则更新成功')
|
||||||
} else {
|
|
||||||
// 添加新规则
|
|
||||||
const createRequest = firewallApi.convertToCreateRequest(rule)
|
|
||||||
await firewallApi.createRule(createRequest)
|
|
||||||
message.success('规则创建成功')
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// 重新加载数据
|
// 添加新规则
|
||||||
await refreshData()
|
rule.id = Date.now().toString()
|
||||||
|
mockRules.value.unshift(rule)
|
||||||
// 重置状态
|
message.success('规则创建成功')
|
||||||
currentEditRule.value = null
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存规则失败:', error)
|
|
||||||
message.error('保存规则失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量操作
|
|
||||||
const handleBatchOperation = async (operation: string) => {
|
|
||||||
if (persistentSelectedRules.value.size === 0) {
|
|
||||||
message.warning('请先选择要操作的规则')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 重置状态
|
||||||
const ruleNames = Array.from(persistentSelectedRules.value)
|
currentEditRule.value = null
|
||||||
switch (operation) {
|
|
||||||
case 'enable':
|
|
||||||
await firewallApi.batchUpdateRules(ruleNames, 'enable')
|
|
||||||
message.success('批量启用成功')
|
|
||||||
break
|
|
||||||
case 'disable':
|
|
||||||
await firewallApi.batchUpdateRules(ruleNames, 'disable')
|
|
||||||
message.success('批量禁用成功')
|
|
||||||
break
|
|
||||||
case 'delete':
|
|
||||||
await firewallApi.batchUpdateRules(ruleNames, 'delete')
|
|
||||||
message.success('批量删除成功')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空选中状态
|
|
||||||
persistentSelectedRules.value.clear()
|
|
||||||
checkedRowKeys.value = []
|
|
||||||
await refreshData()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量操作失败:', error)
|
|
||||||
message.error('批量操作失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRule = async (ruleName: string) => {
|
|
||||||
try {
|
|
||||||
await firewallApi.deleteRule(ruleName)
|
|
||||||
// 从持久化选中状态和当前页面选中状态中移除
|
|
||||||
persistentSelectedRules.value.delete(ruleName)
|
|
||||||
checkedRowKeys.value = checkedRowKeys.value.filter(key => {
|
|
||||||
const itemIndex = parseInt(key.replace('rule-', ''))
|
|
||||||
const rule = getRuleByIndex(itemIndex)
|
|
||||||
return rule?.name !== ruleName
|
|
||||||
})
|
|
||||||
message.success('规则删除成功')
|
|
||||||
|
|
||||||
// 重新获取总数并刷新
|
|
||||||
await refreshData()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除规则失败:', error)
|
|
||||||
message.error('删除规则失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 虚拟滚动相关 - 处理可见区域变化
|
|
||||||
const handleScroll = () => {
|
|
||||||
// RecycleScroller 会自动处理可见区域,我们只需要在项目渲染时按需加载
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听筛选条件变化
|
|
||||||
watch([searchQuery, filters], async () => {
|
|
||||||
await refreshData()
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
|
||||||
onMounted(async () => {
|
|
||||||
await refreshData()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -951,11 +694,6 @@ onMounted(async () => {
|
|||||||
color: var(--accent-error);
|
color: var(--accent-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon-wrapper.info {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
color: var(--accent-info);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-content {
|
.stat-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1465,72 +1203,4 @@ onMounted(async () => {
|
|||||||
:deep(.vue-recycle-scroller__item-wrapper) {
|
:deep(.vue-recycle-scroller__item-wrapper) {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 列表底部状态 */
|
|
||||||
.list-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-top: 1px solid var(--border-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载状态样式 */
|
|
||||||
.loading-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 200px;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.load-more-indicator {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border-top: 1px solid var(--border-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-more-data {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-top: 1px solid var(--border-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空状态样式 */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 300px;
|
|
||||||
gap: 16px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user