A:防火墙增删功能

This commit is contained in:
jimmy committed 2025-07-31 16:35:38 +08:00
1 parent 4f4a3b70c5
commit a99fa53b8c
5 files changed
+489 -164

No files matched your search

@@ -47,3 +47,74 @@ GET http://localhost:8268/api/firewall/rules
# 4 = Public (公共) # 4 = Public (公共)
# 7 = All (所有,即1+2+4) # 7 = All (所有,即1+2+4)
### ✅ 新增防火墙规则
POST http://localhost:你的端口/api/firewall/rules
Content-Type: application/json
{
"name": "MyRule",
"description": "Allow my application",
"applicationName": "C:\\Program Files\\MyApp\\app.exe",
"localAddresses": "*",
"remoteAddresses": "*",
"protocol": "TCP", // 可选值: "TCP", "UDP", "ICMPv4", "ICMPv6"
"icmpTypesAndCodes": "", // 仅当 protocol 为 ICMP 时使用
"localPorts": "8080", // 可为 "80", "80,443", "*" 等
"remotePorts": "*",
"direction": "Inbound", // 可选值: "Inbound", "Outbound"
"enabled": true,
"profiles": "Domain,Private,Public", // 可组合,逗号分隔
"action": "Allow", // 可选值: "Allow", "Block"
"grouping": "MyAppGroup",
"interfaceTypes": "All", // 可选: "All", "Wireless", "LAN", etc.
"edgeTraversal": false
}
### 响应示例:
# {
# "success": true,
# "message": "防火墙规则创建成功",
# "data": null
# }
---
### ✏️ 编辑防火墙规则
PUT http://localhost:你的端口/api/firewall/rules
Content-Type: application/json
{
"currentName": "MyRule",
"newName": "MyRuleUpdated",
"description": "Updated rule",
"enabled": true,
"applicationName": "C:\\Program Files\\MyApp\\new_app.exe",
"protocol": "UDP",
"localPorts": "9999",
"remotePorts": "*",
"localAddresses": "*",
"remoteAddresses": "*",
"profiles": "Private,Public",
"action": "Block",
"grouping": "NewGroup",
"edgeTraversal": false
}
### 响应示例:
# {
# "success": true,
# "message": "防火墙规则更新成功",
# "data": null
# }
---
### ❌ 删除防火墙规则
DELETE http://localhost:你的端口/api/firewall/rules?name=MyRuleUpdated
### 响应示例:
# {
# "success": true,
# "message": "防火墙规则删除成功",
# "data": null
# }
+6 -4
View File
@@ -6,7 +6,7 @@ import type { subRoute, subRouteList } from '@/types/devIndex'
import app from '@/constants/app' import app from '@/constants/app'
import { ref, onMounted, provide } from 'vue' import { ref, onMounted, provide } from 'vue'
import { NButton } from 'naive-ui' import { NButton } from 'naive-ui'
import { NConfigProvider, NGlobalStyle, NMessageProvider, NLoadingBarProvider } from 'naive-ui' import { NConfigProvider, NGlobalStyle, NMessageProvider, NLoadingBarProvider, NDialogProvider } from 'naive-ui'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import NetworkConnectionPanel1 from '@/components/NetworkConnectionPanel1.vue' import NetworkConnectionPanel1 from '@/components/NetworkConnectionPanel1.vue'
import MainLayout from './layouts/MainLayout.vue' import MainLayout from './layouts/MainLayout.vue'
@@ -146,8 +146,9 @@ const getLayout = (route) => {
:theme="themeStore.theme" :theme="themeStore.theme"
> >
<n-message-provider> <n-message-provider>
<n-loading-bar-provider> <n-dialog-provider>
<n-global-style /> <n-loading-bar-provider>
<n-global-style />
<!-- 可拖动的导航栏 --> <!-- 可拖动的导航栏 -->
<div <div
@@ -188,7 +189,8 @@ const getLayout = (route) => {
<!-- <UnifiedConnectionsList4 />--> <!-- <UnifiedConnectionsList4 />-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
</n-loading-bar-provider> </n-loading-bar-provider>
</n-dialog-provider>
</n-message-provider> </n-message-provider>
</n-config-provider> </n-config-provider>
</template> </template>
@@ -240,16 +240,16 @@ import {
FolderOpenOutline, FolderOpenOutline,
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { RuleDirection, RuleAction, ProtocolType, FirewallProfile } from '@/types/firewall' import { RuleDirection, RuleAction, ProtocolType, FirewallProfile } from '@/types/firewall'
import type { DisplayRule } from '@/types/firewall' import type { FirewallRuleShow } from '@/types/firewallFrond'
const props = defineProps<{ const props = defineProps<{
modelValue: boolean modelValue: boolean
editRule?: DisplayRule | null editRule?: FirewallRuleShow | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: boolean] 'update:modelValue': [value: boolean]
save: [rule: DisplayRule] save: [rule: FirewallRuleShow]
}>() }>()
// 响应式数据 // 响应式数据
@@ -265,7 +265,7 @@ const activeTab = ref('basic')
const saving = ref(false) const saving = ref(false)
// 表单数据 // 表单数据
const defaultFormData: DisplayRule = { const defaultFormData: FirewallRuleShow = {
id: '', id: '',
name: '', name: '',
description: '', description: '',
@@ -278,7 +278,7 @@ const defaultFormData: DisplayRule = {
profiles: ['域', '专用'], profiles: ['域', '专用'],
} }
const formData = ref<DisplayRule>({ ...defaultFormData }) const formData = ref<FirewallRuleShow>({ ...defaultFormData })
// 表单验证规则 // 表单验证规则
const formRules: FormRules = { const formRules: FormRules = {
@@ -349,15 +349,14 @@ const handleSave = async () => {
// 生成ID(如果是新规则) // 生成ID(如果是新规则)
if (!formData.value.id) { if (!formData.value.id) {
formData.value.id = Date.now().toString() formData.value.id = crypto.randomUUID()
} }
// 模拟保存延迟 // 触发保存事件,父组件处理实际的API调用
await new Promise((resolve) => setTimeout(resolve, 1000))
emit('save', { ...formData.value }) emit('save', { ...formData.value })
message.success(isEdit.value ? '规则更新成功' : '规则创建成功')
handleClose() // 注意:这里不再自动关闭弹窗和显示成功消息
// 成功/失败的处理将由父组件通过调用 handleSaveSuccess/handleSaveError 来控制
} catch (errors) { } catch (errors) {
message.error('请检查表单中的错误') message.error('请检查表单中的错误')
console.log(errors) console.log(errors)
@@ -366,6 +365,24 @@ const handleSave = async () => {
} }
} }
// 父组件调用此方法表示保存成功
const handleSaveSuccess = () => {
message.success(isEdit.value ? '规则更新成功' : '规则创建成功')
handleClose()
}
// 父组件调用此方法表示保存失败
const handleSaveError = (errorMessage: string) => {
message.error(errorMessage)
// 不关闭弹窗,让用户可以重新尝试
}
// 暴露方法给父组件
defineExpose({
handleSaveSuccess,
handleSaveError
})
const browseApplication = () => { const browseApplication = () => {
// 这里可以集成文件选择器或调用 Electron API // 这里可以集成文件选择器或调用 Electron API
message.info('请使用文件浏览器选择应用程序') message.info('请使用文件浏览器选择应用程序')
@@ -45,3 +45,45 @@ export interface FirewallRulesParam {
port?: number port?: number
search?: string search?: string
} }
/**
* 创建防火墙规则请求
*/
export interface CreateFirewallRuleRequest {
name: string
description?: string
applicationName?: string
localAddresses?: string
remoteAddresses?: string
protocol?: string
icmpTypesAndCodes?: string
localPorts?: string
remotePorts?: string
direction: string
enabled: boolean
profiles: string
action: string
grouping?: string
interfaceTypes?: string
edgeTraversal?: boolean
}
/**
* 更新防火墙规则请求
*/
export interface UpdateFirewallRuleRequest {
currentName: string
newName?: string
description?: string
enabled?: boolean
applicationName?: string
protocol?: string
localPorts?: string
remotePorts?: string
localAddresses?: string
remoteAddresses?: string
profiles?: string
action?: string
grouping?: string
edgeTraversal?: boolean
}
+342 -149
View File
@@ -170,7 +170,9 @@
<span>暂无数据</span> <span>暂无数据</span>
</div> </div>
<div v-if="false" class="data-info"> <div v-if="false" class="data-info">
<span>总共 {{ totalCount }} 条数据(已加载 {{ loadedRanges.size * pageSize }} 条)</span> <span
>总共 {{ totalCount }} 条数据(已加载 {{ loadedRanges.size * pageSize }} 条)</span
>
</div> </div>
<!-- 虚拟滚动列表 --> <!-- 虚拟滚动列表 -->
@@ -181,7 +183,7 @@
:item-size="80" :item-size="80"
:buffer="200" :buffer="200"
key-field="id" key-field="id"
style="height: 500px;" style="height: 500px"
@scroll="onScroll" @scroll="onScroll"
@resize="onResize" @resize="onResize"
@visible="onVisibleRangeUpdate" @visible="onVisibleRangeUpdate"
@@ -189,88 +191,92 @@
> >
<div :key="item.id" class="scroller-item-wrapper"> <div :key="item.id" class="scroller-item-wrapper">
<!-- 占位符项 --> <!-- 占位符项 -->
<div v-if="item.isPlaceholder" class="rule-item loading-placeholder"> <div
v-if="item.isPlaceholder"
class="rule-item loading-placeholder"
:class="{ 'deleted-item': item.isDeleted }"
>
<div class="loading-content"> <div class="loading-content">
<span>加载中...</span> <span>{{ item.isDeleted ? '规则已删除' : '加载中...' }}</span>
</div> </div>
</div> </div>
<!-- 已加载的数据项 --> <!-- 已加载的数据项 -->
<div v-else class="rule-item" :class="{ selected: isItemSelected(item) }"> <div v-else class="rule-item" :class="{ selected: isItemSelected(item) }">
<div class="rule-cell checkbox-cell"> <div class="rule-cell checkbox-cell">
<n-checkbox <n-checkbox
:checked="isItemSelected(item)" :checked="isItemSelected(item)"
@update:checked="(checked) => handleRuleCheck(item.id, checked)" @update:checked="(checked) => handleRuleCheck(item.id, checked)"
/>
</div>
<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>
<div class="rule-cell action-cell"> <div class="rule-cell name-cell">
<n-tag :type="item.action === 'allow' ? 'success' : 'error'" size="small"> <div class="rule-name">{{ item.name }}</div>
{{ item.action === 'allow' ? '允许' : '阻止' }} <div class="rule-description">{{ item.description }}</div>
</n-tag> </div>
</div>
<div class="rule-cell program-cell"> <div class="rule-cell status-cell">
<div class="program-text" :title="item.program">{{ item.program }}</div> <n-tag :type="item.enabled ? 'success' : 'default'" size="small">
</div> {{ item.enabled ? '启用' : '禁用' }}
<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> </n-tag>
</div> </div>
</div>
<div class="rule-cell actions-cell"> <div class="rule-cell direction-cell">
<div class="action-buttons"> <div class="direction-content">
<n-button size="small" quaternary @click="editRule(item)"> <n-icon
<template #icon> :component="item.direction === 'inbound' ? ArrowDownOutline : ArrowUpOutline"
<n-icon :component="CreateOutline" size="14" /> size="16"
</template> />
</n-button> <span>{{ item.direction === 'inbound' ? '入站' : '出站' }}</span>
<n-button size="small" quaternary type="error" @click="deleteRule(item.id)"> </div>
<template #icon> </div>
<n-icon :component="TrashOutline" size="14" />
</template> <div class="rule-cell action-cell">
</n-button> <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>
</div>
</div> </div>
</div> </div>
</RecycleScroller> </RecycleScroller>
@@ -279,7 +285,12 @@
</div> </div>
<!-- 防火墙规则表单 --> <!-- 防火墙规则表单 -->
<FirewallRuleForm v-model="showRuleForm" :edit-rule="currentEditRule" @save="handleSaveRule" /> <FirewallRuleForm
ref="ruleFormRef"
v-model="showRuleForm"
:edit-rule="currentEditRule"
@save="handleSaveRule"
/>
</div> </div>
</template> </template>
@@ -295,6 +306,7 @@ import {
NCheckbox, NCheckbox,
NSelect, NSelect,
useMessage, useMessage,
useDialog,
} from 'naive-ui' } from 'naive-ui'
import { RecycleScroller, DynamicScroller } from 'vue-virtual-scroller' import { RecycleScroller, DynamicScroller } from 'vue-virtual-scroller'
import FirewallRuleForm from '@/components/FirewallRuleForm.vue' import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
@@ -313,20 +325,31 @@ import {
RefreshOutline, RefreshOutline,
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { httpClient } from '@/utils/http' import { httpClient } from '@/utils/http'
import type { FirewallStatus, FirewallRule, RuleDirection, RuleAction, ProtocolType } from '@/types/firewall' import type {
FirewallStatus,
FirewallRule,
RuleDirection,
RuleAction,
ProtocolType,
} from '@/types/firewall'
import { ApiResponse } from '@/types/http' import { ApiResponse } from '@/types/http'
import StringUtils from '@/utils/stringUtils' import StringUtils from '@/utils/stringUtils'
import { import {
FirewallRuleResponse, FirewallRuleResponse,
FirewallRuleShow, FirewallRuleShow,
FirewallRulesParam, FirewallRulesParam,
CreateFirewallRuleRequest,
UpdateFirewallRuleRequest,
} from '@/types/firewallFrond' } from '@/types/firewallFrond'
const message = useMessage() const message = useMessage()
const dialog = useDialog()
// 界面状态 // 界面状态
const showRuleForm = ref(false) const showRuleForm = ref(false)
const currentEditRule = ref(null) const currentEditRule = ref(null)
const ruleFormRef = ref() // 表单组件引用
const originalRuleName = ref<string>('') // 用于跟踪编辑时的原始规则名称
const firewallEnabled = ref(true) const firewallEnabled = ref(true)
const checkedRowKeys = ref<string[]>([]) const checkedRowKeys = ref<string[]>([])
@@ -416,10 +439,11 @@ const loadedPages = new Set<number>()
// 防火墙规则列表 - 预分配所有位置,包含null占位符 // 防火墙规则列表 - 预分配所有位置,包含null占位符
const firewallRule = ref<(FirewallRuleShow | null)[]>([]) const firewallRule = ref<(FirewallRuleShow | null)[]>([])
const loadedRanges = new Set<string>() // 例如:"100-149" const loadedRanges = new Set<string>() // 例如:"100-149"
const deletedIndices = new Set<number>() // 跟踪已删除项目的索引
// 当前已加载的有效数据(非null非占位符) // 当前已加载的有效数据(非null非占位符)
const currentLoadedRules = computed(() => { const currentLoadedRules = computed(() => {
return firewallRule.value.filter(rule => rule !== null) as FirewallRuleShow[] return firewallRule.value.filter((rule) => rule !== null) as FirewallRuleShow[]
}) })
// 为RecycleScroller准备的数据,每个项目都有唯一的id // 为RecycleScroller准备的数据,每个项目都有唯一的id
@@ -441,19 +465,20 @@ const recyclerItems = computed(() => {
port: '', port: '',
profiles: [] as string[], profiles: [] as string[],
isPlaceholder: true as const, isPlaceholder: true as const,
index: index isDeleted: deletedIndices.has(index), // 标记是否为删除的项目
index: index,
} }
} }
}) })
}) })
// 类型守卫函数 // 类型守卫函数
function isPlaceholder(item: any): item is { isPlaceholder: true } { function isPlaceholder(item: unknown): item is { isPlaceholder: true } {
return item && item.isPlaceholder === true return item && item.isPlaceholder === true
} }
// 智能选中状态检查,避免虚拟滚动的选中状态闪烁 // 智能选中状态检查,避免虚拟滚动的选中状态闪烁
function isItemSelected(item: any): boolean { function isItemSelected(item: unknown): boolean {
// 如果是占位符,永远不选中 // 如果是占位符,永远不选中
if (isPlaceholder(item)) { if (isPlaceholder(item)) {
return false return false
@@ -492,14 +517,90 @@ function convertFirewallRuleToShow(rule: FirewallRule): FirewallRuleShow {
} }
} }
// 将前端表单数据转换为API创建请求格式
function convertToCreateRequest(formRule: FirewallRuleShow): CreateFirewallRuleRequest {
return {
name: formRule.name,
description: formRule.description || '',
applicationName: formRule.program === '任意' ? '' : formRule.program,
localAddresses: '*',
remoteAddresses: '*',
protocol: formRule.protocol === '任意' ? 'TCP' : formRule.protocol,
icmpTypesAndCodes: '',
localPorts: formRule.port === '任意' ? '*' : formRule.port,
remotePorts: '*',
direction: formRule.direction === 'inbound' ? 'Inbound' : 'Outbound',
enabled: formRule.enabled,
profiles: formRule.profiles
.map((p) => {
switch (p) {
case '域':
return 'Domain'
case '专用':
return 'Private'
case '公用':
return 'Public'
default:
return p
}
})
.join(','),
action: formRule.action === 'allow' ? 'Allow' : 'Block',
grouping: '',
interfaceTypes: 'All',
edgeTraversal: false,
}
}
// 将前端表单数据转换为API更新请求格式
function convertToUpdateRequest(
formRule: FirewallRuleShow,
originalName: string,
): UpdateFirewallRuleRequest {
return {
currentName: originalName,
newName: formRule.name !== originalName ? formRule.name : undefined,
description: formRule.description || '',
enabled: formRule.enabled,
applicationName: formRule.program === '任意' ? '' : formRule.program,
protocol: formRule.protocol === '任意' ? 'TCP' : formRule.protocol,
localPorts: formRule.port === '任意' ? '*' : formRule.port,
remotePorts: '*',
localAddresses: '*',
remoteAddresses: '*',
profiles: formRule.profiles
.map((p) => {
switch (p) {
case '域':
return 'Domain'
case '专用':
return 'Private'
case '公用':
return 'Public'
default:
return p
}
})
.join(','),
action: formRule.action === 'allow' ? 'Allow' : 'Block',
grouping: '',
edgeTraversal: false,
}
}
// 协议名称转换 // 协议名称转换
function getProtocolName(protocol: ProtocolType): string { function getProtocolName(protocol: ProtocolType): string {
switch (protocol) { switch (protocol) {
case 6: return 'TCP' case 6:
case 17: return 'UDP' return 'TCP'
case 1: return 'ICMPV4' case 17:
case 256: return '任意' return 'UDP'
default: return String(protocol) case 1:
return 'ICMPV4'
case 256:
return '任意'
default:
return String(protocol)
} }
} }
@@ -562,11 +663,16 @@ function buildQueryParams(baseParams: Partial<FirewallRulesParam> = {}): Firewal
// 协议值转换 // 协议值转换
function getProtocolValue(protocol: string): number { function getProtocolValue(protocol: string): number {
switch (protocol) { switch (protocol) {
case 'TCP': return 6 case 'TCP':
case 'UDP': return 17 return 6
case 'ICMPV4': return 1 case 'UDP':
case '任意': return 256 return 17
default: return 256 case 'ICMPV4':
return 1
case '任意':
return 256
default:
return 256
} }
} }
@@ -579,11 +685,11 @@ async function loadRange(startIndex: number, limit = pageSize) {
const rangeKey = `${startIndex}-${startIndex + limit - 1}` const rangeKey = `${startIndex}-${startIndex + limit - 1}`
if (loadedRanges.has(rangeKey)) { if (loadedRanges.has(rangeKey)) {
console.log('范围已加载,跳过:', rangeKey) //console.log('范围已加载,跳过:', rangeKey)
return return
} }
console.log('开始加载数据范围:', rangeKey) //console.log('开始加载数据范围:', rangeKey)
try { try {
loading.value = true loading.value = true
@@ -592,14 +698,14 @@ async function loadRange(startIndex: number, limit = pageSize) {
limit: limit, limit: limit,
}) })
console.log('请求参数:', params) //console.log('请求参数:', params)
const res = await getFirewallRules(params) const res = await getFirewallRules(params)
if (!res) { if (!res) {
console.log('请求返回空结果') console.log('请求返回空结果')
return return
} }
console.log('收到数据:', res.rules.length, '条,总数:', res.totalCount) //console.log('收到数据:', res.rules.length, '条,总数:', res.totalCount)
// 确保数组已预分配 // 确保数组已预分配
if (firewallRule.value.length === 0 && res.totalCount > 0) { if (firewallRule.value.length === 0 && res.totalCount > 0) {
@@ -613,12 +719,12 @@ async function loadRange(startIndex: number, limit = pageSize) {
const index = startIndex + i const index = startIndex + i
if (index < firewallRule.value.length) { if (index < firewallRule.value.length) {
firewallRule.value[index] = convertFirewallRuleToShow(res.rules[i]) firewallRule.value[index] = convertFirewallRuleToShow(res.rules[i])
console.log(`填充数据到位置 ${index}:`, firewallRule.value[index]?.name) //console.log(`填充数据到位置 ${index}:`, firewallRule.value[index]?.name)
} }
} }
loadedRanges.add(rangeKey) loadedRanges.add(rangeKey)
console.log('范围加载完成:', rangeKey, '已加载范围数:', loadedRanges.size) //console.log('范围加载完成:', rangeKey, '已加载范围数:', loadedRanges.size)
} catch (error) { } catch (error) {
console.error('加载数据失败:', error) console.error('加载数据失败:', error)
message.error('数据加载失败') message.error('数据加载失败')
@@ -634,6 +740,7 @@ async function resetAndLoadFirst() {
initialLoading.value = true initialLoading.value = true
firewallRule.value = [] firewallRule.value = []
loadedRanges.clear() loadedRanges.clear()
deletedIndices.clear() // 清理删除标记
// 先获取总数 // 先获取总数
const params = buildQueryParams({ const params = buildQueryParams({
@@ -673,7 +780,7 @@ async function resetAndLoadFirst() {
/** /**
* 滚动事件处理 - 检测是否需要加载更多数据 * 滚动事件处理 - 检测是否需要加载更多数据
*/ */
function onVisibleRangeUpdate(event?: any) { function onVisibleRangeUpdate(event?: unknown) {
console.log('可见范围更新触发:', event) console.log('可见范围更新触发:', event)
// 检查事件参数格式 // 检查事件参数格式
@@ -727,7 +834,7 @@ function onVisibleRangeUpdate(event?: any) {
* 普通滚动事件处理 * 普通滚动事件处理
*/ */
function onScroll(event: Event) { function onScroll(event: Event) {
console.log('滚动事件触发') // console.log('滚动事件触发')
// 尝试基于滚动位置估算可见范围 // 尝试基于滚动位置估算可见范围
const target = event.target as HTMLElement const target = event.target as HTMLElement
@@ -735,30 +842,30 @@ function onScroll(event: Event) {
const { scrollTop, scrollHeight, clientHeight } = target const { scrollTop, scrollHeight, clientHeight } = target
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight const scrollPercentage = (scrollTop + clientHeight) / scrollHeight
console.log('滚动位置信息:', { // console.log('滚动位置信息:', {
scrollTop, // scrollTop,
scrollHeight, // scrollHeight,
clientHeight, // clientHeight,
scrollPercentage: Math.round(scrollPercentage * 100) + '%' // scrollPercentage: Math.round(scrollPercentage * 100) + '%'
}) // })
// 估算当前可见的项目范围 // 估算当前可见的项目范围
const itemHeight = 80 // item-size const itemHeight = 80 // item-size
const visibleStartIndex = Math.floor(scrollTop / itemHeight) const visibleStartIndex = Math.floor(scrollTop / itemHeight)
const visibleEndIndex = Math.ceil((scrollTop + clientHeight) / itemHeight) const visibleEndIndex = Math.ceil((scrollTop + clientHeight) / itemHeight)
console.log('估算的可见范围:', { visibleStartIndex, visibleEndIndex }) // console.log('估算的可见范围:', { visibleStartIndex, visibleEndIndex })
// 计算需要加载的数据段 // 计算需要加载的数据段
const alignedStart = Math.floor(visibleStartIndex / pageSize) * pageSize const alignedStart = Math.floor(visibleStartIndex / pageSize) * pageSize
const alignedEnd = Math.ceil((visibleEndIndex + 1) / pageSize) * pageSize const alignedEnd = Math.ceil((visibleEndIndex + 1) / pageSize) * pageSize
console.log('基于滚动位置的加载范围:', alignedStart, alignedEnd) // console.log('基于滚动位置的加载范围:', alignedStart, alignedEnd)
// 加载可见范围内的数据段 // 加载可见范围内的数据段
for (let i = alignedStart; i < alignedEnd; i += pageSize) { for (let i = alignedStart; i < alignedEnd; i += pageSize) {
if (i < totalCount.value) { if (i < totalCount.value) {
console.log('滚动位置触发加载范围:', i, 'to', i + pageSize - 1) // console.log('滚动位置触发加载范围:', i, 'to', i + pageSize - 1)
loadRange(i, pageSize) loadRange(i, pageSize)
} }
} }
@@ -776,11 +883,15 @@ function onResize() {
resetAndLoadFirst() resetAndLoadFirst()
// 监听搜索和过滤条件变化,重新加载数据 // 监听搜索和过滤条件变化,重新加载数据
watch([searchQuery, filters], () => { watch(
// 搜索条件变化时清理选中状态,避免状态不一致 [searchQuery, filters],
checkedRowKeys.value = [] () => {
resetAndLoadFirst() // 搜索条件变化时清理选中状态,避免状态不一致
}, { deep: true }) checkedRowKeys.value = []
resetAndLoadFirst()
},
{ deep: true },
)
// 全选相关计算属性 // 全选相关计算属性
const allSelected = computed({ const allSelected = computed({
@@ -789,30 +900,30 @@ const allSelected = computed({
if (currentValidRules.length === 0) { if (currentValidRules.length === 0) {
return false return false
} }
// 检查当前已加载数据中有多少被选中 // 检查当前已加载数据中有多少被选中
const selectedInCurrentData = currentValidRules.filter(rule => const selectedInCurrentData = currentValidRules.filter((rule) =>
checkedRowKeys.value.includes(rule.id) checkedRowKeys.value.includes(rule.id),
) )
return selectedInCurrentData.length === currentValidRules.length return selectedInCurrentData.length === currentValidRules.length
}, },
set: (value: boolean) => { set: (value: boolean) => {
if (value) { if (value) {
// 选中当前已加载的所有有效数据 // 选中当前已加载的所有有效数据
const currentValidRules = currentLoadedRules.value const currentValidRules = currentLoadedRules.value
const currentIds = currentValidRules.map(rule => rule.id) const currentIds = currentValidRules.map((rule) => rule.id)
// 合并现有选中和当前页选中(去重) // 合并现有选中和当前页选中(去重)
const mergedIds = [...new Set([...checkedRowKeys.value, ...currentIds])] const mergedIds = [...new Set([...checkedRowKeys.value, ...currentIds])]
checkedRowKeys.value = mergedIds checkedRowKeys.value = mergedIds
} else { } else {
// 取消选中当前已加载的数据 // 取消选中当前已加载的数据
const currentValidRules = currentLoadedRules.value const currentValidRules = currentLoadedRules.value
const currentIds = new Set(currentValidRules.map(rule => rule.id)) const currentIds = new Set(currentValidRules.map((rule) => rule.id))
// 只保留不在当前页的选中项 // 只保留不在当前页的选中项
checkedRowKeys.value = checkedRowKeys.value.filter(id => !currentIds.has(id)) checkedRowKeys.value = checkedRowKeys.value.filter((id) => !currentIds.has(id))
} }
}, },
}) })
@@ -822,12 +933,12 @@ const indeterminate = computed(() => {
if (currentValidRules.length === 0) { if (currentValidRules.length === 0) {
return false return false
} }
// 检查当前已加载数据中有多少被选中 // 检查当前已加载数据中有多少被选中
const selectedInCurrentData = currentValidRules.filter(rule => const selectedInCurrentData = currentValidRules.filter((rule) =>
checkedRowKeys.value.includes(rule.id) checkedRowKeys.value.includes(rule.id),
) )
return selectedInCurrentData.length > 0 && selectedInCurrentData.length < currentValidRules.length return selectedInCurrentData.length > 0 && selectedInCurrentData.length < currentValidRules.length
}) })
@@ -838,23 +949,57 @@ const openRuleForm = (rule: unknown = null) => {
} }
const editRule = (rule: unknown) => { const editRule = (rule: unknown) => {
currentEditRule.value = { ...rule } const ruleToEdit = rule as FirewallRuleShow
currentEditRule.value = { ...ruleToEdit }
originalRuleName.value = ruleToEdit.name // 记录原始名称用于API调用
showRuleForm.value = true showRuleForm.value = true
} }
const deleteRule = async (id: string) => { const deleteRule = async (id: string) => {
// TODO: 实现删除规则API调用 // 从本地数据中找到对应的规则
console.log('删除规则:', id) const ruleToDelete = firewallRule.value.find((rule) => rule?.id === id)
if (!ruleToDelete) {
// 从本地数据中移除(临时方案,应该调用API) message.error('找不到要删除的规则')
const index = firewallRule.value.findIndex((rule) => rule?.id === id) return
if (index !== -1) {
firewallRule.value[index] = null // 设为null而不是删除,保持索引不变
totalCount.value = Math.max(0, totalCount.value - 1)
// 从选中项中移除
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
message.success('规则删除成功')
} }
// 显示确认对话框
dialog.warning({
title: '确认删除',
content: `您确定要删除防火墙规则"${ruleToDelete.name}"吗?此操作不可撤销。`,
positiveText: '确认删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
console.log('删除规则:', ruleToDelete.name)
// 调用删除API
const res: ApiResponse<null> = await httpClient.delete(
`/firewall/rules?name=${encodeURIComponent(ruleToDelete.name)}`,
)
if (!res.success) {
const errorMsg = StringUtils.isBlank(res.message) ? '删除规则失败!' : res.message
message.error(errorMsg)
return
}
// 从本地数据中移除
const index = firewallRule.value.findIndex((rule) => rule?.id === id)
if (index !== -1) {
firewallRule.value[index] = null // 设为null而不是删除,保持索引不变
deletedIndices.add(index) // 标记这个索引为已删除
totalCount.value = Math.max(0, totalCount.value - 1)
// 从选中项中移除
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
message.success('规则删除成功')
}
} catch (error) {
console.error('删除规则失败:', error)
message.error('删除规则时发生错误')
}
},
})
} }
const handleRuleCheck = (id: string, checked: boolean) => { const handleRuleCheck = (id: string, checked: boolean) => {
@@ -886,24 +1031,60 @@ const clearFilters = () => {
} }
const handleSaveRule = async (rule: unknown) => { const handleSaveRule = async (rule: unknown) => {
// TODO: 实现保存规则API调用 try {
console.log('保存规则:', rule) const formRule = rule as FirewallRuleShow
console.log('保存规则:', formRule)
if (rule.id) { if (formRule.id && originalRuleName.value) {
// 编辑现有规则 // 编辑现有规则
const index = firewallRule.value.findIndex((r) => r?.id === rule.id) console.log('更新规则:', originalRuleName.value, '→', formRule.name)
if (index !== -1) {
firewallRule.value[index] = rule as FirewallRuleShow const updateRequest = convertToUpdateRequest(formRule, originalRuleName.value)
message.success('规则更新成功') const res: ApiResponse<null> = await httpClient.put('/firewall/rules', updateRequest)
if (!res.success) {
const errorMsg = StringUtils.isBlank(res.message) ? '更新规则失败!' : res.message
// 调用表单组件的错误处理方法,不关闭弹窗
ruleFormRef.value?.handleSaveError(errorMsg)
return
}
// 更新本地数据
const index = firewallRule.value.findIndex((r) => r?.id === formRule.id)
if (index !== -1) {
firewallRule.value[index] = formRule
}
// 调用表单组件的成功处理方法,关闭弹窗
ruleFormRef.value?.handleSaveSuccess()
} else {
// 创建新规则
console.log('创建新规则:', formRule.name)
const createRequest = convertToCreateRequest(formRule)
const res: ApiResponse<null> = await httpClient.post('/firewall/rules', createRequest)
if (!res.success) {
const errorMsg = StringUtils.isBlank(res.message) ? '创建规则失败!' : res.message
// 调用表单组件的错误处理方法,不关闭弹窗
ruleFormRef.value?.handleSaveError(errorMsg)
return
}
// 调用表单组件的成功处理方法,关闭弹窗
ruleFormRef.value?.handleSaveSuccess()
// 重新加载数据以获取新的总数和数据
resetAndLoadFirst()
} }
} else {
// 添加新规则 - 这里需要重新加载数据,因为数组大小固定
message.success('规则创建成功,正在刷新数据...')
resetAndLoadFirst() // 重新加载以获取新的总数和数据
}
// 重置状态 // 重置状态
currentEditRule.value = null currentEditRule.value = null
originalRuleName.value = ''
} catch (error) {
console.error('保存规则失败:', error)
// 调用表单组件的错误处理方法,不关闭弹窗
ruleFormRef.value?.handleSaveError('保存规则时发生错误')
}
} }
</script> </script>
@@ -1790,7 +1971,9 @@ const handleSaveRule = async (rule: unknown) => {
} }
/* 调试信息样式 */ /* 调试信息样式 */
.loading-info, .no-data, .data-info { .loading-info,
.no-data,
.data-info {
padding: 20px; padding: 20px;
text-align: center; text-align: center;
color: var(--text-secondary); color: var(--text-secondary);
@@ -1814,6 +1997,16 @@ const handleSaveRule = async (rule: unknown) => {
opacity: 0.6; opacity: 0.6;
} }
.loading-placeholder.deleted-item {
background: rgba(239, 68, 68, 0.1);
border-left: 3px solid var(--accent-error);
}
.loading-placeholder.deleted-item .loading-content {
color: var(--accent-error);
font-weight: 500;
}
.loading-content { .loading-content {
display: flex; display: flex;
align-items: center; align-items: center;