A:防火墙展示列表功能重建

This commit is contained in:
jimmy committed 2025-07-28 16:44:18 +08:00
1 parent 4ef0d8cb89
commit 1491757e7f
8 files changed
+698 -313

No files matched your search

+1 -4
View File
@@ -1,5 +1,2 @@
VITE_MODE='development'
VITE_APP_API_URL='http://localhost:8268/api'
我想让FirewallRuleForm.vue的滚动区域,只在页面主容器内进行,比如“基础内容”很多,那么就在“基础内容”的
子Tab中进行滚动,不能超出去,不然滚动时将会整体滚动,非常不优雅,而且你这次优化的样式我觉得还有提升空间,你可以参考MonitorView下的组件进行样式优化
VITE_APP_API_URL='http://localhost:8268/api'
+5
View File
@@ -14,10 +14,15 @@ import { useUuidStore } from '@/stores/uuidStore'
import { logB } from '@/utils/logHelper/logUtils'
import { httpConfig } from '@/config/httpConfig'
// Vue Virtual Scroller
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import VueVirtualScroller from 'vue-virtual-scroller'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(websocketPlugin)
app.use(VueVirtualScroller)
// 使用 主题 支持
app.use(createNaiveUI())
@@ -11,12 +11,12 @@
import type { arrayUtilsNotEmptyCallback } from '@/types/utils'
export default class ArrayUtils {
/**
* 指定数组或未知元素是否为 `null` 或 `empty`
* @param arr
*/
static isEmpty (arr: any[] | null): boolean {
static isEmpty (arr: unknown[] | null): boolean {
return arr === null || arr.length === 0
}
}
+671 -288
View File
@@ -17,21 +17,13 @@
<template #icon>
<n-icon :component="AddOutline" />
</template>
新建规则
<span class="button-text">新建规则</span>
</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="stats-grid stats-hidden-on-small">
<div class="stat-card">
<div class="stat-icon-wrapper primary">
<n-icon :component="ListOutline" size="24" />
@@ -81,7 +73,6 @@
<n-input
v-model:value="searchQuery"
placeholder="搜索规则名称..."
size="small"
clearable
style="width: 280px"
>
@@ -89,57 +80,195 @@
<n-icon :component="SearchOutline" />
</template>
</n-input>
<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="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 class="filter-section">
<div class="filter-group">
<span class="filter-label">方向</span>
<n-select
v-model:value="filters.direction"
size="small"
style="width: 120px"
placeholder="全部"
clearable
:options="directionOptions"
/>
</div>
<div class="filter-group">
<span class="filter-label">操作</span>
<n-select
v-model:value="filters.action"
size="small"
style="width: 120px"
placeholder="全部"
clearable
:options="actionOptions"
/>
</div>
<div class="filter-group">
<span class="filter-label">状态</span>
<n-select
v-model:value="filters.enabled"
size="small"
style="width: 120px"
placeholder="全部"
clearable
:options="enabledOptions"
/>
</div>
<div class="filter-group">
<span class="filter-label">协议</span>
<n-select
v-model:value="filters.protocol"
size="small"
style="width: 120px"
placeholder="全部"
clearable
:options="protocolOptions"
/>
</div>
<div class="filter-group">
<n-button size="small" quaternary @click="clearFilters">
<template #icon>
<n-icon :component="RefreshOutline" />
</template>
重置筛选
</n-button>
</div>
</div>
<!-- 虚拟滚动规则列表 -->
<div class="rules-list-container">
<!-- 列表头部 -->
<div class="list-header">
<div class="header-cell checkbox-cell">
<n-checkbox
v-model:checked="allSelected"
:indeterminate="indeterminate"
@update:checked="handleSelectAll"
/>
</div>
<div class="header-cell name-cell">规则名称</div>
<div class="header-cell status-cell">状态</div>
<div class="header-cell direction-cell">方向</div>
<div class="header-cell action-cell">操作</div>
<div class="header-cell program-cell">程序/服务</div>
<div class="header-cell protocol-cell">协议</div>
<div class="header-cell port-cell">端口</div>
<div class="header-cell profiles-cell">配置文件</div>
<div class="header-cell actions-cell">操作</div>
</div>
<!-- 虚拟滚动列表 -->
<RecycleScroller
class="rules-scroller"
:items="filteredRules"
:item-size="80"
key-field="id"
v-slot="{ item }"
>
<div class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
<div class="rule-cell checkbox-cell">
<n-checkbox
:checked="checkedRowKeys.includes(item.id)"
@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 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>
</div>
</div>
</div>
<!-- 防火墙规则表单 -->
<FirewallRuleForm
v-model="showRuleForm"
:edit-rule="currentEditRule"
@save="handleSaveRule"
/>
<FirewallRuleForm v-model="showRuleForm" :edit-rule="currentEditRule" @save="handleSaveRule" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, h } from 'vue'
import {
NButton,
NIcon,
NInput,
NSwitch,
NTag,
NDataTable,
import {
NButton,
NIcon,
NInput,
NSwitch,
NTag,
NDropdown,
NCheckbox,
NSelect,
useMessage,
type DataTableColumns
} from 'naive-ui'
import { RecycleScroller } from 'vue-virtual-scroller'
import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
import {
AddOutline,
@@ -149,12 +278,11 @@ import {
CloseCircleOutline,
ListOutline,
ChevronDownOutline,
FilterOutline,
CreateOutline,
CopyOutline,
TrashOutline,
ArrowUpOutline,
ArrowDownOutline,
RefreshOutline,
} from '@vicons/ionicons5'
const message = useMessage()
@@ -167,55 +295,52 @@ const checkedRowKeys = ref<string[]>([])
// 搜索和过滤
const searchQuery = ref('')
const filters = ref({
direction: null as string | null,
action: null as string | null,
enabled: null as boolean | null,
protocol: null as string | null,
})
// 过滤选项
const directionOptions = [
{ label: '入站', value: 'inbound' },
{ label: '出站', value: 'outbound' },
]
const actionOptions = [
{ label: '允许', value: 'allow' },
{ label: '阻止', value: 'block' },
]
const enabledOptions = [
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
]
const protocolOptions = [
{ label: 'TCP', value: 'TCP' },
{ label: 'UDP', value: 'UDP' },
{ label: '任意', value: '任意' },
]
// 批量操作选项
const batchOptions = [
{
label: '启用选中',
key: 'enable',
icon: () => h(NIcon, { component: CheckmarkCircleOutline })
icon: () => h(NIcon, { component: CheckmarkCircleOutline }),
},
{
label: '禁用选中',
label: '禁用选中',
key: 'disable',
icon: () => h(NIcon, { component: PauseCircleOutline })
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' }
]
icon: () => h(NIcon, { component: TrashOutline }),
},
{
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' }
]
}
]
// 统计数据
@@ -242,7 +367,7 @@ const mockRules = ref([
priority: 100,
},
{
id: '2',
id: '2',
name: 'Microsoft Edge',
description: '允许浏览器访问网络',
enabled: true,
@@ -262,7 +387,7 @@ const mockRules = ref([
direction: 'inbound',
action: 'block',
program: '任意',
protocol: 'TCP',
protocol: 'TCP',
port: '23',
profiles: ['域', '专用', '公用'],
priority: 300,
@@ -285,7 +410,7 @@ const mockRules = ref([
name: 'HTTP 服务器',
description: '允许 Web 服务器接收请求',
enabled: true,
direction: 'inbound',
direction: 'inbound',
action: 'allow',
program: '任意',
protocol: 'TCP',
@@ -293,172 +418,126 @@ const mockRules = ref([
profiles: ['域', '专用', '公用'],
priority: 500,
},
// 添加更多模拟数据以测试虚拟滚动
...Array.from({ length: 50 }, (_, i) => ({
id: `${i + 6}`,
name: `规则 ${i + 6}`,
description: `这是第 ${i + 6} 个测试规则`,
enabled: Math.random() > 0.5,
direction: Math.random() > 0.5 ? 'inbound' : 'outbound',
action: Math.random() > 0.5 ? 'allow' : 'block',
program: `程序 ${i + 6}`,
protocol: ['TCP', 'UDP', '任意'][Math.floor(Math.random() * 3)],
port: Math.floor(Math.random() * 65535).toString(),
profiles: ['域', '专用', '公用'].slice(0, Math.floor(Math.random() * 3) + 1),
priority: (i + 6) * 100,
})),
])
// 表格列定义
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)
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({
get: () => {
return (
filteredRules.value.length > 0 && checkedRowKeys.value.length === filteredRules.value.length
)
},
set: (value: boolean) => {
if (value) {
checkedRowKeys.value = filteredRules.value.map((rule) => rule.id)
} else {
checkedRowKeys.value = []
}
},
})
const indeterminate = computed(() => {
return checkedRowKeys.value.length > 0 && checkedRowKeys.value.length < filteredRules.value.length
})
// 方法
const openRuleForm = (rule: any = null) => {
const openRuleForm = (rule: unknown = null) => {
currentEditRule.value = rule
showRuleForm.value = true
}
const editRule = (rule: any) => {
const editRule = (rule: unknown) => {
currentEditRule.value = { ...rule }
showRuleForm.value = true
}
const deleteRule = (id: string) => {
const index = mockRules.value.findIndex(rule => rule.id === id)
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 handleCheck = (keys: string[]) => {
checkedRowKeys.value = keys
const handleRuleCheck = (id: string, checked: boolean) => {
if (checked) {
if (!checkedRowKeys.value.includes(id)) {
checkedRowKeys.value.push(id)
}
} else {
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
}
}
const handleSaveRule = (rule: any) => {
const handleSelectAll = (checked: boolean) => {
allSelected.value = checked
}
const clearFilters = () => {
filters.value = {
direction: null,
action: null,
enabled: null,
protocol: null,
}
}
const handleSaveRule = (rule: unknown) => {
if (rule.id) {
// 编辑现有规则
const index = mockRules.value.findIndex(r => r.id === rule.id)
const index = mockRules.value.findIndex((r) => r.id === rule.id)
if (index !== -1) {
mockRules.value[index] = rule
message.success('规则更新成功')
@@ -469,7 +548,7 @@ const handleSaveRule = (rule: any) => {
mockRules.value.unshift(rule)
message.success('规则创建成功')
}
// 重置状态
currentEditRule.value = null
}
@@ -477,15 +556,21 @@ const handleSaveRule = (rule: any) => {
<style scoped>
.firewall-view {
height: 100%;
overflow-y: auto;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
overflow: hidden;
}
.firewall-container {
padding: 24px;
max-width: 1400px;
max-width: 1500px;
margin: 0 auto;
height: 100%;
display: flex;
flex-direction: column;
min-width: 0; /* 允许容器缩小 */
}
/* 工具栏 */
@@ -493,12 +578,15 @@ const handleSaveRule = (rule: any) => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 24px;
margin-bottom: 8px;
gap: 16px;
flex-shrink: 0;
min-width: 0; /* 允许子元素缩小 */
}
.toolbar-left {
flex: 1;
min-width: 0; /* 允许内容缩小 */
}
.view-title {
@@ -506,18 +594,26 @@ const handleSaveRule = (rule: any) => {
font-weight: 700;
color: var(--text-primary);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.view-subtitle {
font-size: 14px;
color: var(--text-muted);
margin: 4px 0 0 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
min-width: 0; /* 允许内容缩小 */
}
.firewall-status {
@@ -528,6 +624,8 @@ const handleSaveRule = (rule: any) => {
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 8px;
white-space: nowrap;
flex-shrink: 0;
}
.status-label {
@@ -536,12 +634,17 @@ const handleSaveRule = (rule: any) => {
white-space: nowrap;
}
.button-text {
white-space: nowrap;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 24px;
margin-bottom: 8px;
flex-shrink: 0;
}
.stat-card {
@@ -549,7 +652,7 @@ const handleSaveRule = (rule: any) => {
backdrop-filter: var(--backdrop-blur);
border: 1px solid var(--border-primary);
border-radius: 12px;
padding: 20px;
padding: 16px;
display: flex;
align-items: center;
gap: 16px;
@@ -568,6 +671,7 @@ const handleSaveRule = (rule: any) => {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-icon-wrapper.primary {
@@ -592,6 +696,7 @@ const handleSaveRule = (rule: any) => {
.stat-content {
flex: 1;
min-width: 0;
}
.stat-number {
@@ -614,14 +719,22 @@ const handleSaveRule = (rule: any) => {
border: 1px solid var(--border-primary);
border-radius: 12px;
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
margin-bottom: 50px;
min-width: 0; /* 允许面板缩小 */
}
.panel-header {
padding: 20px 24px;
padding: 10px 16px;
border-bottom: 1px solid var(--border-primary);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
min-width: 0;
}
.panel-title {
@@ -629,23 +742,206 @@ const handleSaveRule = (rule: any) => {
font-weight: 600;
color: var(--text-primary);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.panel-controls {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
/* 表格容器 */
.table-container {
padding: 16px 24px 24px;
/* 优化后的筛选区域 */
.filter-section {
padding: 16px 24px;
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
gap: 20px;
background: var(--bg-tertiary);
flex-shrink: 0;
flex-wrap: wrap;
overflow-x: auto;
}
/* 表格单元格样式 */
.rule-name-cell {
.filter-group {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.filter-label {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
min-width: 40px;
}
/* 规则列表容器 */
.rules-list-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0; /* 允许容器缩小 */
overflow: hidden;
}
/* 列表头部 */
.list-header {
display: flex;
align-items: center;
padding: 0 16px;
height: 40px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
flex-shrink: 0;
min-width: 0; /* 允许头部缩小 */
overflow: hidden;
}
.header-cell {
display: flex;
align-items: center;
padding: 0 8px;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 基础列宽 - 针对超大屏幕(>1400px) */
@media (min-width: 1401px) {
.checkbox-cell { width: 50px; }
.name-cell { width: 200px; }
.status-cell { width: 80px; }
.direction-cell { width: 80px; }
.action-cell { width: 80px; }
.program-cell { width: 250px; flex-shrink: 1; }
.protocol-cell { width: 80px; }
.port-cell { width: 100px; }
.profiles-cell { width: 150px; flex-shrink: 1; }
.actions-cell { width: 100px; }
}
/* 大屏幕 1201px-1400px */
@media (min-width: 1201px) and (max-width: 1400px) {
.checkbox-cell { width: 45px; }
.name-cell { width: 180px; }
.status-cell { width: 70px; }
.direction-cell { width: 70px; }
.action-cell { width: 70px; }
.program-cell { width: 220px; flex-shrink: 1; }
.protocol-cell { width: 70px; }
.port-cell { width: 90px; }
.profiles-cell { width: 130px; flex-shrink: 1; }
.actions-cell { width: 90px; }
}
/* 中等屏幕 1025px-1200px */
@media (min-width: 1025px) and (max-width: 1200px) {
.checkbox-cell { width: 40px; }
.name-cell { width: 160px; }
.status-cell { width: 65px; }
.direction-cell { width: 65px; }
.action-cell { width: 65px; }
.program-cell { width: 200px; flex-shrink: 1; }
.protocol-cell { width: 65px; }
.port-cell { width: 80px; }
.profiles-cell { width: 120px; flex-shrink: 1; }
.actions-cell { width: 85px; }
}
/* 小屏幕 769px-1024px */
@media (min-width: 769px) and (max-width: 1024px) {
.checkbox-cell { width: 40px; }
.name-cell { width: 140px; }
.status-cell { width: 60px; }
.direction-cell { width: 60px; }
.action-cell { width: 60px; }
.program-cell { width: 180px; flex-shrink: 1; }
.protocol-cell { width: 60px; }
.port-cell { width: 80px; }
.profiles-cell { width: 100px; flex-shrink: 1; }
.actions-cell { width: 80px; }
}
/* 虚拟滚动列表 */
.rules-scroller {
flex: 1;
min-height: 0;
height: 100%;
overflow: auto;
}
.rule-item {
display: flex;
align-items: center;
padding: 0 16px;
height: 80px;
border-bottom: 1px solid var(--border-secondary);
transition: var(--transition);
background: var(--bg-card);
min-width: 0; /* 允许项目缩小 */
}
.rule-item:hover {
background: var(--bg-tertiary);
}
.rule-item.selected {
background: rgba(59, 130, 246, 0.05);
border-left: 3px solid var(--accent-primary);
}
/* 斑马纹效果 */
.rule-item:nth-child(even) {
background: rgba(0, 0, 0, 0.02);
}
.rule-item:nth-child(odd) {
background: var(--bg-card);
}
/* 暗色模式下的斑马纹效果 */
@media (prefers-color-scheme: dark) {
.rule-item:nth-child(even) {
background: rgba(255, 255, 255, 0.03);
}
}
.rule-item:hover {
background: var(--bg-tertiary) !important;
}
.rule-item.selected {
background: rgba(59, 130, 246, 0.05) !important;
border-left: 3px solid var(--accent-primary);
}
.rule-cell {
display: flex;
align-items: center;
padding: 0 8px;
min-height: 60px;
font-size: 13px;
flex-shrink: 0;
overflow: hidden;
min-width: 0; /* 允许单元格缩小 */
}
.rule-cell.name-cell {
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 4px;
}
@@ -653,111 +949,177 @@ const handleSaveRule = (rule: any) => {
font-weight: 600;
color: var(--text-primary);
font-size: 14px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.rule-description {
font-size: 12px;
color: var(--text-muted);
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.direction-cell {
.direction-content {
display: flex;
align-items: center;
font-size: 13px;
gap: 6px;
color: var(--text-secondary);
min-width: 0;
}
.profiles-cell {
.direction-content span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.program-text {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-primary);
}
.profiles-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
max-width: 100%;
overflow: hidden;
}
.profile-tag {
font-size: 11px;
padding: 2px 6px;
flex-shrink: 0;
}
.action-buttons {
display: flex;
gap: 8px;
gap: 6px;
flex-shrink: 0;
}
/* 响应式 */
@media (max-width: 1200px) {
/* 响应式设计 */
/* 在小屏幕上隐藏统计卡片 */
@media (max-width: 1024px), (max-height: 700px) {
.stats-hidden-on-small {
display: none !important;
}
}
/* 顶部工具栏响应式 */
@media (max-width: 900px) {
.firewall-toolbar {
flex-direction: column;
align-items: stretch;
gap: 16px;
gap: 12px;
}
.toolbar-right {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
}
.view-subtitle {
display: none;
}
}
@media (max-width: 600px) {
.status-label {
display: none;
}
.button-text {
display: none;
}
.firewall-status {
padding: 8px 12px;
}
}
/* 超小屏幕特殊处理 */
@media (max-width: 768px) {
.firewall-container {
padding: 16px;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
.panel-controls {
display: none;
}
.filter-section {
padding: 12px 16px;
gap: 12px;
}
.stat-card {
.filter-group {
gap: 6px;
}
.filter-label {
font-size: 12px;
min-width: 35px;
}
/* 隐藏部分列 */
.program-cell,
.profiles-cell {
display: none;
}
.checkbox-cell { width: 35px; }
.name-cell { width: 120px; }
.status-cell { width: 50px; }
.direction-cell { width: 50px; }
.action-cell { width: 50px; }
.protocol-cell { width: 50px; }
.port-cell { width: 60px; }
.actions-cell { width: 70px; }
}
/* 高度不足时的优化 */
@media (max-height: 600px) {
.firewall-container {
padding: 16px;
}
.stat-number {
font-size: 24px;
.view-title {
font-size: 18px;
}
.view-subtitle {
display: none;
}
.firewall-toolbar {
margin-bottom: 4px;
}
.rules-panel {
margin-bottom: 20px;
}
.panel-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 8px 16px;
}
.panel-controls {
width: 100%;
justify-content: space-between;
}
.toolbar-right {
flex-direction: column;
gap: 8px;
}
.firewall-status {
width: 100%;
justify-content: space-between;
.filter-section {
padding: 8px 16px;
}
}
/* 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;
}
/* Naive UI 组件样式覆盖 */
:deep(.n-input) {
--n-border: 1px solid var(--border-tertiary);
--n-border-hover: 1px solid var(--border-hover);
@@ -766,6 +1128,14 @@ const handleSaveRule = (rule: any) => {
--n-text-color: var(--text-primary);
}
:deep(.n-select) {
--n-color: var(--bg-card);
--n-border: 1px solid var(--border-tertiary);
--n-border-hover: 1px solid var(--border-hover);
--n-border-focus: 1px solid var(--accent-primary);
--n-text-color: var(--text-primary);
}
:deep(.n-dropdown-menu) {
--n-color: var(--bg-card);
--n-option-text-color: var(--text-primary);
@@ -779,4 +1149,17 @@ const handleSaveRule = (rule: any) => {
font-weight: 500;
font-size: 12px;
}
:deep(.n-checkbox) {
font-size: 14px;
}
/* vue-virtual-scroller 样式 */
:deep(.vue-recycle-scroller) {
height: 100%;
}
:deep(.vue-recycle-scroller__item-wrapper) {
overflow: visible;
}
</style>
@@ -573,7 +573,7 @@ const stats = computed(() => {
// 数据处理 - 将 ConnectionsTable1 的数据转换为 UnifiedConnectionsList4 的格式
const originalItems = computed(() => {
const result: any[] = []
const result:unknown[] = []
if (!props.connections || !Array.isArray(props.connections) || props.connections.length === 0) {
return result
@@ -629,8 +629,8 @@ const originalItems = computed(() => {
// 显示数据
const displayItems = computed(() => {
const result: any[] = []
const processGroups: { [key: number]: any[] } = {}
const result:unknown[] = []
const processGroups: { [key: number]:unknown[] } = {}
originalItems.value.forEach((item) => {
const processIndex = item.processIndex
@@ -701,7 +701,7 @@ const shouldShowSticky = computed(() => {
return currentIndex > stickyIndex
})
const shouldHideItem = (item: any, index: number) => {
const shouldHideItem = (item:unknown, index: number) => {
if (!stickyItem.value || !shouldShowSticky.value) return false
return item.key === stickyItem.value.key
}
@@ -883,9 +883,9 @@ const getTimeSinceActive = (lastActive: Date): string => {
return `${Math.floor(diff / 3600000)}小时前`
}
const getProcessStatus = (process: any) => {
const getProcessStatus = (process:unknown) => {
if (process.hasExited) return { type: 'error' as const, text: '已退出', show: true }
const hasActiveConnections = process.connections?.some((c: any) => c.isActive)
const hasActiveConnections = process.connections?.some((c:unknown) => c.isActive)
if (hasActiveConnections) {
return { type: 'success' as const, text: '活跃', show: true }
}
@@ -206,7 +206,7 @@ let scrollTimer: NodeJS.Timeout | null = null
// 工具函数
const debounce = (func: Function, wait: number) => {
let timeout: NodeJS.Timeout
return function executedFunction(...args: any[]) {
return function executedFunction(...args:unknown[]) {
const later = () => {
clearTimeout(timeout)
func(...args)
@@ -220,7 +220,7 @@ const throttle = (func: Function, delay: number) => {
let timeoutId: NodeJS.Timeout
let lastExecTime = 0
return function (...args: any[]) {
return function (...args:unknown[]) {
const currentTime = Date.now()
if (currentTime - lastExecTime > delay) {
@@ -343,7 +343,7 @@ const getConnectionStatusType = (state: number) => {
// 数据处理
const originalItems = computed(() => {
const result: any[] = []
const result:unknown[] = []
if (!ap || !Array.isArray(ap) || ap.length === 0) {
return result
@@ -384,8 +384,8 @@ const originalItems = computed(() => {
// 显示数据
const displayItems = computed(() => {
const result: any[] = []
const processGroups: { [key: number]: any[] } = {}
const result:unknown[] = []
const processGroups: { [key: number]:unknown[] } = {}
originalItems.value.forEach((item) => {
const processIndex = item.processIndex
@@ -450,7 +450,7 @@ const shouldShowSticky = computed(() => {
return currentIndex > stickyIndex
})
const shouldHideItem = (item: any, index: number) => {
const shouldHideItem = (itemunknown, index: number) => {
if (!stickyItem.value || !shouldShowSticky.value) return false
return item.key === stickyItem.value.key
}
@@ -383,7 +383,7 @@ const formatBytes = (bytes: number) => {
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
}
const formatEndpoint = (endpoint: any) => {
const formatEndpoint = (endpoint:unknown) => {
return `${endpoint.address}:${endpoint.port}`
}
@@ -434,8 +434,8 @@ const getConnectionStateType = (state: number, isActive: boolean) => {
return types[state as keyof typeof types] || 'default'
}
const getProcessStatus = (process: any) => {
const hasActiveConnections = process.connections?.some((c: any) => c.isActive)
const getProcessStatus = (processunknown) => {
const hasActiveConnections = process.connections?.some((cunknown) => c.isActive)
if (hasActiveConnections) {
return { type: 'success', text: '活跃', show: true }
}
@@ -444,7 +444,7 @@ const getProcessStatus = (process: any) => {
// 数据处理
const originalItems = computed(() => {
const result: any[] = []
const result:unknown[] = []
if (!ap || !Array.isArray(ap) || ap.length === 0) {
return result
@@ -486,8 +486,8 @@ const originalItems = computed(() => {
// 显示数据
const displayItems = computed(() => {
const result: any[] = []
const processGroups: { [key: number]: any[] } = {}
const result:unknown[] = []
const processGroups: { [key: number]:unknown[] } = {}
originalItems.value.forEach((item) => {
const processIndex = item.processIndex
@@ -552,7 +552,7 @@ const shouldShowSticky = computed(() => {
return currentIndex > stickyIndex
})
const shouldHideItem = (item: any, index: number) => {
const shouldHideItem = (item:unknown, index: number) => {
if (!stickyItem.value || !shouldShowSticky.value) return false
return item.key === stickyItem.value.key
}