This commit is contained in:
jimmy committed 2025-07-30 15:04:10 +08:00
1 parent e265261fa0
commit 0ab7a2bca4
3 files changed
+250 -579

No files matched your search

+1 -1
View File
@@ -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
+248 -578
View File
@@ -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>