Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
ee5b83bb9d
31 files changed
+2222
-3898
No files matched your search
@@ -14,9 +14,13 @@
|
||||
[English](#english) | [中文](#chinese)
|
||||
</div>
|
||||
|
||||
> **目前项目正在积极开发阶段, 在 `1.0` 版本前不保证项目可用性.**
|
||||
>
|
||||
> **The project is currently in active development, and there is no guarantee of project availability until the '1.0' release.**
|
||||
<p style="color: red; font-weight: bold;">
|
||||
🚧 当前项目正在积极开发阶段,在 <code>1.0</code> 版本前不保证项目可用性。
|
||||
</p>
|
||||
|
||||
<p style="color: red; font-weight: bold;">
|
||||
🚧 The project is currently in active development, and there is no guarantee of project availability until the <code>1.0</code> release.
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Common.Utils
|
||||
totalSpeed.UploadSpeed += speed.UploadSpeed;
|
||||
}
|
||||
|
||||
Log.Info($"总网速 - 下载: {totalSpeed.DownloadSpeedText}, 上传: {totalSpeed.UploadSpeedText}");
|
||||
// Log.Info($"总网速 - 下载: {totalSpeed.DownloadSpeedText}, 上传: {totalSpeed.UploadSpeedText}");
|
||||
return totalSpeed;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace Common.Utils
|
||||
// 跳过环回接口
|
||||
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback)
|
||||
{
|
||||
Log.Info($"跳过环回接口: {ni.Name}");
|
||||
//Log.Info($"跳过环回接口: {ni.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ namespace Common.Utils
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info($"总网速 - 下载: {totalSpeed.DownloadSpeedText}, 上传: {totalSpeed.UploadSpeedText}");
|
||||
// Log.Info($"总网速 - 下载: {totalSpeed.DownloadSpeedText}, 上传: {totalSpeed.UploadSpeedText}");
|
||||
|
||||
return totalSpeed;
|
||||
}
|
||||
@@ -308,4 +308,4 @@ namespace Common.Utils
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATraceEvent_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F25b12134b1124ffbab73641db2e54e5b310830_003F4d_003F09175cdc_003FTraceEvent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWebSocketServer_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa7d96aff8e344c1d9aa8042f229d493cae00_003Fc6_003F2f09550a_003FWebSocketServer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWebView2_002Ecs_002Fl_003AC_0021_003FUsers_003Fcharlatans_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8aca2f2c90e1454c825fc8b69c53a97e14268_003F06_003F22dc0e77_003FWebView2_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWindow_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd079e839ea2040f09d8282baf64f01edf61928_003Fab_003F96a4af57_003FWindow_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f4a8c497_002D43fb_002D4de1_002D9ffa_002D84076d7db2c5/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>NUnit3x::B3F481C9-B351-449C-AE2B-CDEC29BD5DCC::net8.0::Common.Test.Tests.Test1</TestId>
|
||||
|
||||
@@ -20,12 +20,16 @@ using Infrastructure.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Events;
|
||||
using Shell;
|
||||
using Shell.UserControls;
|
||||
using Shell.Utils;
|
||||
using Shell.Views;
|
||||
using Utils.ETW.Etw;
|
||||
using Utils.Firewall;
|
||||
|
||||
namespace NetVizor;
|
||||
|
||||
using System.Windows.Forms; // 引用 Windows Forms 命名空间
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
@@ -42,16 +46,20 @@ public partial class App : System.Windows.Application
|
||||
|
||||
// 启动服务(如 WebSocket、HTTP、端口监听等)
|
||||
// StartMyServer();
|
||||
|
||||
// NetView
|
||||
// var netView = new NetView();
|
||||
// netView.Show();
|
||||
|
||||
Mouth();
|
||||
// NetView 网络监看
|
||||
var netView = new NetView();
|
||||
netView.Show();
|
||||
|
||||
// Mouth();
|
||||
|
||||
|
||||
// 网速监视
|
||||
// var window = new NetView();
|
||||
// window.Show();
|
||||
|
||||
// 设置应用程序在关闭最后一个窗口时不自动退出
|
||||
this.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
}
|
||||
|
||||
private async void Mouth()
|
||||
@@ -59,7 +67,7 @@ public partial class App : System.Windows.Application
|
||||
var firewallService = new FirewallService();
|
||||
// 防火墙状态
|
||||
// var statusAsync = await firewallService.GetStatusAsync();
|
||||
var rulesAsync =await firewallService.GetRulesAsync();
|
||||
var rulesAsync = await firewallService.GetRulesAsync();
|
||||
Console.WriteLine(JsonHelper.ToJson(rulesAsync));
|
||||
}
|
||||
|
||||
@@ -405,6 +413,10 @@ public partial class App : System.Windows.Application
|
||||
|
||||
#endregion
|
||||
|
||||
#region 获取防火墙信息
|
||||
|
||||
#endregion
|
||||
|
||||
#region 取消订阅
|
||||
|
||||
server.Post("/api/unsubscribe", async (context) =>
|
||||
@@ -516,4 +528,4 @@ public class SubscriptionAppInfo : SubscriptionInfo
|
||||
// 再有,“协议和端口”中的协议类型也有很多,我们也要展示。比如它甚至能指定用户,可以选择用户和组。
|
||||
// 所以基于上,我们几乎是重写了防火墙的页面。
|
||||
// 我现在需要你基于这个能获取到的信息(可能有些我没提到),使用html完成一个防火墙的看板,要点击某个规则可以编辑,可以新增。
|
||||
// 新增的窗口和交互都需要完成,数据的展示也需要完成。你用假数据替代数据填充即可,要求界面要现代化,美观,优雅
|
||||
// 新增的窗口和交互都需要完成,数据的展示也需要完成。你用假数据替代数据填充即可,要求界面要现代化,美观,优雅
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -6,14 +6,22 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms> <!-- 加上这句,才能正确启用 Forms 支持 -->
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\Icons\"/>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3240.44" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0-preview.6.25358.103"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Forms"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -30,6 +38,10 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="WinDivert.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="Assets\Icons\app.ico"/>
|
||||
<Content Include="Assets\Icons\app.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Shell.UserControls
|
||||
/// <summary>
|
||||
/// Interaction logic for NetworkMonitorView.xaml
|
||||
/// </summary>
|
||||
public partial class NetworkMonitorView : UserControl
|
||||
public partial class NetworkMonitorView : System.Windows.Controls.UserControl
|
||||
{
|
||||
private NetworkMonitorViewModel _viewModel;
|
||||
|
||||
@@ -38,4 +38,4 @@ namespace Shell.UserControls
|
||||
_viewModel?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<Window x:Class="Shell.UserControls.TrayMenuWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Shell.UserControls"
|
||||
mc:Ignorable="d"
|
||||
Title="TrayMenu"
|
||||
Height="200" Width="180"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
Deactivated="Window_Deactivated">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 菜单项样式 -->
|
||||
<Style x:Key="MenuItemStyle" TargetType="Border">
|
||||
<Setter Property="Padding" Value="15,8" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#E3F2FD" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- 图标样式 -->
|
||||
<Style x:Key="IconStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets" />
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="Foreground" Value="#2196F3" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,0,10,0" />
|
||||
</Style>
|
||||
|
||||
<!-- 文本样式 -->
|
||||
<Style x:Key="TextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="#333333" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- 分隔线样式 -->
|
||||
<Style x:Key="SeparatorStyle" TargetType="Rectangle">
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Fill" Value="#E0E0E0" />
|
||||
<Setter Property="Margin" Value="10,5" />
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border Background="White"
|
||||
CornerRadius="8"
|
||||
BorderBrush="#E0E0E0"
|
||||
BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect ShadowDepth="3"
|
||||
Direction="315"
|
||||
Color="#40000000"
|
||||
Opacity="0.3"
|
||||
BlurRadius="10" />
|
||||
</Border.Effect>
|
||||
|
||||
<StackPanel Margin="5">
|
||||
<!-- 打开主窗口 -->
|
||||
<Border Style="{StaticResource MenuItemStyle}"
|
||||
MouseLeftButtonDown="ShowMainWindow_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" Style="{StaticResource IconStyle}" />
|
||||
<TextBlock Text="打开主窗口" Style="{StaticResource TextStyle}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Rectangle Style="{StaticResource SeparatorStyle}" />
|
||||
|
||||
<!-- 最小化到托盘 -->
|
||||
<Border Style="{StaticResource MenuItemStyle}"
|
||||
MouseLeftButtonDown="HideToTray_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" Style="{StaticResource IconStyle}" />
|
||||
<TextBlock Text="最小化到托盘" Style="{StaticResource TextStyle}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 关于 -->
|
||||
<Border Style="{StaticResource MenuItemStyle}"
|
||||
MouseLeftButtonDown="About_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" Style="{StaticResource IconStyle}" />
|
||||
<TextBlock Text="关于" Style="{StaticResource TextStyle}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Rectangle Style="{StaticResource SeparatorStyle}" />
|
||||
|
||||
<!-- 退出程序 -->
|
||||
<Border Style="{StaticResource MenuItemStyle}"
|
||||
MouseLeftButtonDown="ExitApplication_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
Style="{StaticResource IconStyle}"
|
||||
Foreground="#F44336" />
|
||||
<TextBlock Text="退出程序"
|
||||
Style="{StaticResource TextStyle}"
|
||||
Foreground="#F44336" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,256 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Shell.UserControls;
|
||||
|
||||
public partial class TrayMenuWindow : Window
|
||||
{
|
||||
public event Action OnShowMainWindow;
|
||||
public event Action OnExitApplication;
|
||||
public event Action OnHideToTray;
|
||||
|
||||
// 菜单的固定尺寸(与XAML中设置的一致)
|
||||
private const double MENU_WIDTH = 180;
|
||||
private const double MENU_HEIGHT = 200;
|
||||
|
||||
public TrayMenuWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.Loaded += TrayMenuWindow_Loaded;
|
||||
|
||||
// 确保窗口使用Per-Monitor DPI Awareness
|
||||
this.SourceInitialized += (s, e) =>
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
var source = HwndSource.FromHwnd(hwnd);
|
||||
if (source != null)
|
||||
{
|
||||
source.AddHook(WndProc);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
// 处理DPI变化消息
|
||||
const int WM_DPICHANGED = 0x02E0;
|
||||
if (msg == WM_DPICHANGED)
|
||||
{
|
||||
// DPI发生变化时可以在这里处理
|
||||
Console.WriteLine("DPI changed detected");
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private void TrayMenuWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 添加淡入动画
|
||||
// var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
|
||||
// this.BeginAnimation(OpacityProperty, fadeIn);
|
||||
|
||||
// 添加缩放动画
|
||||
// var scaleTransform = new System.Windows.Media.ScaleTransform(0.8, 0.8);
|
||||
// this.RenderTransform = scaleTransform;
|
||||
// this.RenderTransformOrigin = new System.Windows.Point(0.5, 0.5);
|
||||
|
||||
// var scaleXAnimation = new DoubleAnimation(0.8, 1, TimeSpan.FromMilliseconds(200));
|
||||
// var scaleYAnimation = new DoubleAnimation(0.8, 1, TimeSpan.FromMilliseconds(200));
|
||||
|
||||
// scaleXAnimation.EasingFunction = new QuadraticEase();
|
||||
// scaleYAnimation.EasingFunction = new QuadraticEase();
|
||||
|
||||
// scaleTransform.BeginAnimation(System.Windows.Media.ScaleTransform.ScaleXProperty, scaleXAnimation);
|
||||
// scaleTransform.BeginAnimation(System.Windows.Media.ScaleTransform.ScaleYProperty, scaleYAnimation);
|
||||
}
|
||||
|
||||
public void ShowAt(int x, int y)
|
||||
{
|
||||
// 使用主显示器工作区域的旧方法(保留向后兼容)
|
||||
var workArea = SystemParameters.WorkArea;
|
||||
var customWorkArea = new Shell.Utils.TrayIconManager.RECT
|
||||
{
|
||||
Left = (int)workArea.Left,
|
||||
Top = (int)workArea.Top,
|
||||
Right = (int)workArea.Right,
|
||||
Bottom = (int)workArea.Bottom
|
||||
};
|
||||
ShowAtWithWorkArea(x, y, customWorkArea);
|
||||
}
|
||||
|
||||
public void ShowAtWithWorkArea(int x, int y, Shell.Utils.TrayIconManager.RECT workArea)
|
||||
{
|
||||
Console.WriteLine("=== TrayMenu ShowAtWithWorkArea Debug ===");
|
||||
Console.WriteLine($"输入鼠标位置: X={x}, Y={y}");
|
||||
Console.WriteLine(
|
||||
$"使用工作区域: Left={workArea.Left}, Top={workArea.Top}, Right={workArea.Right}, Bottom={workArea.Bottom}");
|
||||
Console.WriteLine($"工作区域尺寸: Width={workArea.Right - workArea.Left}, Height={workArea.Bottom - workArea.Top}");
|
||||
Console.WriteLine($"菜单尺寸: Width={MENU_WIDTH}, Height={MENU_HEIGHT}");
|
||||
|
||||
// 获取当前窗口的DPI缩放信息
|
||||
var dpiScale = GetDpiScale();
|
||||
Console.WriteLine($"窗口DPI缩放: X={dpiScale.DpiScaleX:F2}, Y={dpiScale.DpiScaleY:F2}");
|
||||
|
||||
// 计算菜单显示位置 - 优先显示在鼠标左上方
|
||||
double left = x - MENU_WIDTH;
|
||||
double top = y - MENU_HEIGHT;
|
||||
|
||||
Console.WriteLine($"初始计算位置 (左上方): Left={left}, Top={top}");
|
||||
|
||||
// 智能位置调整算法
|
||||
var adjustedPosition = CalculateOptimalPosition(x, y, workArea, MENU_WIDTH, MENU_HEIGHT);
|
||||
left = adjustedPosition.X;
|
||||
top = adjustedPosition.Y;
|
||||
|
||||
Console.WriteLine($"智能调整后位置: Left={left}, Top={top}");
|
||||
|
||||
// 最终边界检查(确保完全在工作区域内)
|
||||
left = Math.Max(workArea.Left, Math.Min(left, workArea.Right - MENU_WIDTH));
|
||||
top = Math.Max(workArea.Top, Math.Min(top, workArea.Bottom - MENU_HEIGHT));
|
||||
|
||||
Console.WriteLine($"最终边界检查后位置: Left={left}, Top={top}");
|
||||
|
||||
// 设置窗口位置
|
||||
this.Left = left;
|
||||
this.Top = top;
|
||||
|
||||
// 显示窗口
|
||||
this.Show();
|
||||
this.Activate();
|
||||
this.Focus();
|
||||
|
||||
// 验证实际设置的位置
|
||||
Console.WriteLine($"窗口实际位置: Left={this.Left}, Top={this.Top}");
|
||||
Console.WriteLine($"窗口实际尺寸: Width={this.ActualWidth}, Height={this.ActualHeight}");
|
||||
Console.WriteLine("=== Debug End ===\n");
|
||||
}
|
||||
|
||||
private (double X, double Y) CalculateOptimalPosition(int mouseX, int mouseY,
|
||||
Shell.Utils.TrayIconManager.RECT workArea, double menuWidth, double menuHeight)
|
||||
{
|
||||
double left, top;
|
||||
|
||||
// 定义四个可能的位置:左上、右上、左下、右下
|
||||
var positions = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
X = (double)(mouseX - menuWidth), Y = (double)(mouseY - menuHeight), Priority = 1, Name = "左上"
|
||||
}, // 左上 (优先)
|
||||
new { X = (double)mouseX, Y = (double)(mouseY - menuHeight), Priority = 2, Name = "右上" }, // 右上
|
||||
new { X = (double)(mouseX - menuWidth), Y = (double)mouseY, Priority = 3, Name = "左下" }, // 左下
|
||||
new { X = (double)mouseX, Y = (double)mouseY, Priority = 4, Name = "右下" } // 右下
|
||||
};
|
||||
|
||||
// 检查每个位置是否完全在工作区域内
|
||||
foreach (var pos in positions.OrderBy(p => p.Priority))
|
||||
{
|
||||
bool fitsHorizontally = pos.X >= workArea.Left && pos.X + menuWidth <= workArea.Right;
|
||||
bool fitsVertically = pos.Y >= workArea.Top && pos.Y + menuHeight <= workArea.Bottom;
|
||||
|
||||
Console.WriteLine(
|
||||
$"检查位置 {pos.Name}: X={pos.X:F1}, Y={pos.Y:F1}, 水平适合={fitsHorizontally}, 垂直适合={fitsVertically}");
|
||||
|
||||
if (fitsHorizontally && fitsVertically)
|
||||
{
|
||||
Console.WriteLine($"选择位置: {pos.Name}");
|
||||
return (pos.X, pos.Y);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有位置完全适合,使用智能调整
|
||||
Console.WriteLine("没有完全适合的位置,使用智能调整");
|
||||
|
||||
// 计算最佳的水平位置
|
||||
if (mouseX - menuWidth >= workArea.Left)
|
||||
{
|
||||
left = mouseX - menuWidth; // 左侧有足够空间
|
||||
}
|
||||
else if (mouseX + menuWidth <= workArea.Right)
|
||||
{
|
||||
left = mouseX; // 右侧有足够空间
|
||||
}
|
||||
else
|
||||
{
|
||||
// 两侧都不够,居中显示或贴边
|
||||
left = Math.Max(workArea.Left, workArea.Right - menuWidth);
|
||||
}
|
||||
|
||||
// 计算最佳的垂直位置
|
||||
if (mouseY - menuHeight >= workArea.Top)
|
||||
{
|
||||
top = mouseY - menuHeight; // 上方有足够空间
|
||||
}
|
||||
else if (mouseY + menuHeight <= workArea.Bottom)
|
||||
{
|
||||
top = mouseY; // 下方有足够空间
|
||||
}
|
||||
else
|
||||
{
|
||||
// 上下都不够,贴顶部或底部
|
||||
top = Math.Max(workArea.Top, workArea.Bottom - menuHeight);
|
||||
}
|
||||
|
||||
Console.WriteLine($"智能调整结果: Left={left:F1}, Top={top:F1}");
|
||||
return (left, top);
|
||||
}
|
||||
|
||||
private System.Windows.DpiScale GetDpiScale()
|
||||
{
|
||||
try
|
||||
{
|
||||
var source = PresentationSource.FromVisual(this);
|
||||
if (source?.CompositionTarget != null)
|
||||
{
|
||||
return VisualTreeHelper.GetDpi(this);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"获取DPI缩放失败: {ex.Message}");
|
||||
}
|
||||
|
||||
// 返回默认缩放
|
||||
return new System.Windows.DpiScale(1.0, 1.0);
|
||||
}
|
||||
|
||||
private void ShowMainWindow_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
OnShowMainWindow?.Invoke();
|
||||
this.Hide();
|
||||
}
|
||||
|
||||
private void HideToTray_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
OnHideToTray?.Invoke();
|
||||
this.Hide();
|
||||
}
|
||||
|
||||
private void About_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
System.Windows.MessageBox.Show("这是一个带有系统托盘功能的WPF应用程序。", "关于",
|
||||
System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
|
||||
this.Hide();
|
||||
}
|
||||
|
||||
private void ExitApplication_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var result = System.Windows.MessageBox.Show("确定要退出程序吗?", "确认退出",
|
||||
System.Windows.MessageBoxButton.YesNo, System.Windows.MessageBoxImage.Question);
|
||||
if (result == System.Windows.MessageBoxResult.Yes)
|
||||
{
|
||||
OnExitApplication?.Invoke();
|
||||
}
|
||||
|
||||
this.Hide();
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
// 当窗口失去焦点时自动隐藏
|
||||
this.Hide();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Infrastructure.utils;
|
||||
|
||||
namespace Shell.UserControls;
|
||||
|
||||
public partial class WebPanel : UserControl
|
||||
public partial class WebPanel : System.Windows.Controls.UserControl
|
||||
{
|
||||
public WebPanel()
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public partial class WebPanel : UserControl
|
||||
switch (msg?.Channel)
|
||||
{
|
||||
case "showMessage":
|
||||
MessageBox.Show(msg.Payload?.Content ?? "无内容", msg.Payload?.Title ?? "提示");
|
||||
System.Windows.MessageBox.Show(msg.Payload?.Content ?? "无内容", msg.Payload?.Title ?? "提示");
|
||||
InitWebView1();
|
||||
break;
|
||||
case "GetNetInfo":
|
||||
@@ -175,4 +175,4 @@ public class JsData
|
||||
{
|
||||
return $"Title:{Title}, Content:{Content}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Shell.UserControls;
|
||||
|
||||
namespace Shell.Utils;
|
||||
|
||||
public class TrayIconManager : IDisposable
|
||||
{
|
||||
private NotifyIcon _notifyIcon;
|
||||
private Window _mainWindow;
|
||||
private TrayMenuWindow _trayMenu;
|
||||
private DispatcherTimer _clickTimer;
|
||||
private bool _isDoubleClick = false;
|
||||
|
||||
// Win32 API 用于获取鼠标位置和显示器信息
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||
|
||||
// 添加DPI相关API
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetDpiForWindow(IntPtr hwnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetDpiForSystem();
|
||||
|
||||
[DllImport("shcore.dll")]
|
||||
private static extern int GetDpiForMonitor(IntPtr hmonitor, DpiType dpiType, out uint dpiX, out uint dpiY);
|
||||
|
||||
public enum DpiType
|
||||
{
|
||||
Effective = 0,
|
||||
Angular = 1,
|
||||
Raw = 2,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MONITORINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
private const uint MONITOR_DEFAULTTONEAREST = 2;
|
||||
|
||||
public TrayIconManager(Window mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
InitializeTrayIcon();
|
||||
InitializeClickTimer();
|
||||
}
|
||||
|
||||
private void InitializeTrayIcon()
|
||||
{
|
||||
_notifyIcon = new NotifyIcon
|
||||
{
|
||||
Icon = LoadIcon(),
|
||||
Visible = true,
|
||||
Text = "你的应用程序"
|
||||
};
|
||||
|
||||
_notifyIcon.MouseClick += OnTrayIconClick;
|
||||
_notifyIcon.MouseDoubleClick += OnTrayIconDoubleClick;
|
||||
}
|
||||
|
||||
private void InitializeClickTimer()
|
||||
{
|
||||
_clickTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(300)
|
||||
};
|
||||
_clickTimer.Tick += OnClickTimerTick;
|
||||
}
|
||||
|
||||
private Icon LoadIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Icon("Assets/Icons/app.ico");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return SystemIcons.Application;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTrayIconClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Right)
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowContextMenu();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
_isDoubleClick = false;
|
||||
_clickTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTrayIconDoubleClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
_isDoubleClick = true;
|
||||
_clickTimer.Stop();
|
||||
ShowMainWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClickTimerTick(object sender, EventArgs e)
|
||||
{
|
||||
_clickTimer.Stop();
|
||||
if (!_isDoubleClick)
|
||||
{
|
||||
// 单击逻辑(可选)
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowMainWindow()
|
||||
{
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
_mainWindow.Show();
|
||||
_mainWindow.WindowState = WindowState.Normal;
|
||||
_mainWindow.Activate();
|
||||
_mainWindow.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowContextMenu()
|
||||
{
|
||||
Console.WriteLine("=== TrayIconManager ShowContextMenu ===");
|
||||
|
||||
if (_trayMenu == null)
|
||||
{
|
||||
_trayMenu = new TrayMenuWindow();
|
||||
_trayMenu.OnShowMainWindow += () => ShowMainWindow();
|
||||
_trayMenu.OnExitApplication += () => System.Windows.Application.Current.Shutdown();
|
||||
_trayMenu.OnHideToTray += () => _mainWindow?.Hide();
|
||||
Console.WriteLine("创建新的TrayMenuWindow");
|
||||
}
|
||||
|
||||
// 使用Win32 API获取鼠标位置
|
||||
if (GetCursorPos(out POINT mousePos))
|
||||
{
|
||||
Console.WriteLine($"Win32 API获取鼠标物理位置: X={mousePos.X}, Y={mousePos.Y}");
|
||||
|
||||
// 获取鼠标所在显示器的工作区域和DPI信息
|
||||
var (workArea, dpiScaleX, dpiScaleY) = GetMouseMonitorInfoWithDpi(mousePos);
|
||||
Console.WriteLine(
|
||||
$"显示器工作区域: Left={workArea.Left}, Top={workArea.Top}, Right={workArea.Right}, Bottom={workArea.Bottom}");
|
||||
Console.WriteLine($"DPI缩放比例: ScaleX={dpiScaleX:F2}, ScaleY={dpiScaleY:F2}");
|
||||
|
||||
// 将物理坐标转换为WPF逻辑坐标
|
||||
var logicalMouseX = mousePos.X / dpiScaleX;
|
||||
var logicalMouseY = mousePos.Y / dpiScaleY;
|
||||
|
||||
// 将工作区域也转换为逻辑坐标
|
||||
var logicalWorkArea = new RECT
|
||||
{
|
||||
Left = (int)(workArea.Left / dpiScaleX),
|
||||
Top = (int)(workArea.Top / dpiScaleY),
|
||||
Right = (int)(workArea.Right / dpiScaleX),
|
||||
Bottom = (int)(workArea.Bottom / dpiScaleY)
|
||||
};
|
||||
|
||||
Console.WriteLine($"逻辑鼠标位置: X={logicalMouseX:F1}, Y={logicalMouseY:F1}");
|
||||
Console.WriteLine(
|
||||
$"逻辑工作区域: Left={logicalWorkArea.Left}, Top={logicalWorkArea.Top}, Right={logicalWorkArea.Right}, Bottom={logicalWorkArea.Bottom}");
|
||||
|
||||
_trayMenu.ShowAtWithWorkArea((int)logicalMouseX, (int)logicalMouseY, logicalWorkArea);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("获取鼠标位置失败,使用默认位置");
|
||||
// 如果获取鼠标位置失败,使用托盘图标的估计位置
|
||||
var workArea = SystemParameters.WorkArea;
|
||||
var defaultX = (int)workArea.Right - 50;
|
||||
var defaultY = (int)workArea.Bottom - 50;
|
||||
Console.WriteLine($"使用默认位置: X={defaultX}, Y={defaultY}");
|
||||
_trayMenu.ShowAt(defaultX, defaultY);
|
||||
}
|
||||
|
||||
Console.WriteLine("=== ShowContextMenu End ===\n");
|
||||
}
|
||||
|
||||
private (RECT workArea, double dpiScaleX, double dpiScaleY) GetMouseMonitorInfoWithDpi(POINT mousePos)
|
||||
{
|
||||
// 获取鼠标所在的显示器
|
||||
IntPtr hMonitor = MonitorFromPoint(mousePos, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
// 获取显示器信息
|
||||
MONITORINFO monitorInfo = new MONITORINFO();
|
||||
monitorInfo.cbSize = (uint)Marshal.SizeOf(monitorInfo);
|
||||
|
||||
RECT workArea;
|
||||
double dpiScaleX = 1.0;
|
||||
double dpiScaleY = 1.0;
|
||||
|
||||
if (GetMonitorInfo(hMonitor, ref monitorInfo))
|
||||
{
|
||||
workArea = monitorInfo.rcWork;
|
||||
|
||||
// 获取该显示器的DPI
|
||||
try
|
||||
{
|
||||
if (GetDpiForMonitor(hMonitor, DpiType.Effective, out uint dpiX, out uint dpiY) == 0)
|
||||
{
|
||||
dpiScaleX = dpiX / 96.0; // 96 DPI是标准DPI
|
||||
dpiScaleY = dpiY / 96.0;
|
||||
Console.WriteLine($"显示器DPI: X={dpiX}, Y={dpiY}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果GetDpiForMonitor失败,尝试使用系统DPI
|
||||
var systemDpi = GetDpiForSystem();
|
||||
dpiScaleX = dpiScaleY = systemDpi / 96.0;
|
||||
Console.WriteLine($"使用系统DPI: {systemDpi}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"获取DPI失败: {ex.Message},使用默认缩放");
|
||||
// 作为后备方案,使用WPF的DPI信息
|
||||
var source = PresentationSource.FromVisual(System.Windows.Application.Current.MainWindow);
|
||||
if (source?.CompositionTarget != null)
|
||||
{
|
||||
var matrix = source.CompositionTarget.TransformFromDevice;
|
||||
dpiScaleX = 1.0 / matrix.M11;
|
||||
dpiScaleY = 1.0 / matrix.M22;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果获取失败,返回主显示器的工作区域
|
||||
var wpfWorkArea = SystemParameters.WorkArea;
|
||||
workArea = new RECT
|
||||
{
|
||||
Left = (int)wpfWorkArea.Left,
|
||||
Top = (int)wpfWorkArea.Top,
|
||||
Right = (int)wpfWorkArea.Right,
|
||||
Bottom = (int)wpfWorkArea.Bottom
|
||||
};
|
||||
|
||||
// 使用WPF的DPI信息作为后备
|
||||
var source = PresentationSource.FromVisual(System.Windows.Application.Current.MainWindow);
|
||||
if (source?.CompositionTarget != null)
|
||||
{
|
||||
var matrix = source.CompositionTarget.TransformFromDevice;
|
||||
dpiScaleX = 1.0 / matrix.M11;
|
||||
dpiScaleY = 1.0 / matrix.M22;
|
||||
}
|
||||
}
|
||||
|
||||
return (workArea, dpiScaleX, dpiScaleY);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_notifyIcon?.Dispose();
|
||||
_trayMenu?.Close();
|
||||
_clickTimer?.Stop();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,41 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using Shell.Utils;
|
||||
|
||||
namespace Shell.Views;
|
||||
|
||||
public partial class NetView : Window
|
||||
{
|
||||
private TrayIconManager _trayIconManager;
|
||||
|
||||
public NetView()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeTrayIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeTrayIcon()
|
||||
{
|
||||
_trayIconManager = new TrayIconManager(this);
|
||||
}
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
// 阻止窗口关闭,而是隐藏到托盘
|
||||
e.Cancel = true;
|
||||
this.Hide();
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_trayIconManager?.Dispose();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
// 如果你想要真正关闭程序的方法
|
||||
public void ForceClose()
|
||||
{
|
||||
_trayIconManager?.Dispose();
|
||||
System.Windows.Application.Current.Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
/// Windows防火墙策略对象,用于访问防火墙的全局设置和状态
|
||||
/// </summary>
|
||||
private readonly INetFwPolicy2? _fwPolicy;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Windows防火墙规则集合对象,用于管理防火墙规则
|
||||
/// </summary>
|
||||
@@ -73,9 +73,9 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
|
||||
// 获取规则统计信息
|
||||
var allRules = GetAllRules();
|
||||
status.TotalRules = allRules.Count; // 总规则数
|
||||
status.EnabledRules = allRules.Count(r => r.Enabled); // 已启用规则数
|
||||
status.InboundRules = allRules.Count(r => r.Direction == RuleDirection.Inbound); // 入站规则数
|
||||
status.TotalRules = allRules.Count; // 总规则数
|
||||
status.EnabledRules = allRules.Count(r => r.Enabled); // 已启用规则数
|
||||
status.InboundRules = allRules.Count(r => r.Direction == RuleDirection.Inbound); // 入站规则数
|
||||
status.OutboundRules = allRules.Count(r => r.Direction == RuleDirection.Outbound); // 出站规则数
|
||||
|
||||
return status;
|
||||
@@ -164,12 +164,12 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
return new ProfileStatus
|
||||
{
|
||||
Profile = profile,
|
||||
IsEnabled = _fwPolicy.FirewallEnabled[profileType], // 是否启用
|
||||
BlockAllInboundTraffic = _fwPolicy.BlockAllInboundTraffic[profileType], // 是否阻止所有入站流量
|
||||
NotifyOnListen = _fwPolicy.NotificationsDisabled[profileType] == false, // 是否显示通知
|
||||
IsEnabled = _fwPolicy.FirewallEnabled[profileType], // 是否启用
|
||||
BlockAllInboundTraffic = _fwPolicy.BlockAllInboundTraffic[profileType], // 是否阻止所有入站流量
|
||||
NotifyOnListen = _fwPolicy.NotificationsDisabled[profileType] == false, // 是否显示通知
|
||||
UnicastResponsesDisabled = _fwPolicy.UnicastResponsesToMulticastBroadcastDisabled[profileType], // 单播响应设置
|
||||
DefaultInboundAction = (RuleAction)_fwPolicy.DefaultInboundAction[profileType], // 默认入站动作
|
||||
DefaultOutboundAction = (RuleAction)_fwPolicy.DefaultOutboundAction[profileType] // 默认出站动作
|
||||
DefaultInboundAction = (RuleAction)_fwPolicy.DefaultInboundAction[profileType], // 默认入站动作
|
||||
DefaultOutboundAction = (RuleAction)_fwPolicy.DefaultOutboundAction[profileType] // 默认出站动作
|
||||
};
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
if (_fwPolicy == null) return false;
|
||||
|
||||
var profileType = ConvertToNetFwProfile(profile);
|
||||
|
||||
|
||||
// 设置各项配置
|
||||
_fwPolicy.FirewallEnabled[profileType] = status.IsEnabled;
|
||||
_fwPolicy.BlockAllInboundTraffic[profileType] = status.BlockAllInboundTraffic;
|
||||
@@ -194,7 +194,7 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
_fwPolicy.UnicastResponsesToMulticastBroadcastDisabled[profileType] = status.UnicastResponsesDisabled;
|
||||
_fwPolicy.DefaultInboundAction[profileType] = (NET_FW_ACTION_)status.DefaultInboundAction;
|
||||
_fwPolicy.DefaultOutboundAction[profileType] = (NET_FW_ACTION_)status.DefaultOutboundAction;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
@@ -219,7 +219,8 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
// 遍历Windows防火墙中的所有规则并转换为自定义格式
|
||||
foreach (INetFwRule rule in _fwRules)
|
||||
{
|
||||
rules.Add(ConvertToFirewallRule(rule));
|
||||
var convertToFirewallRule = ConvertToFirewallRule(rule);
|
||||
rules.Add(convertToFirewallRule);
|
||||
}
|
||||
|
||||
return rules;
|
||||
@@ -358,22 +359,22 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
if (newRule == null) return false;
|
||||
|
||||
// 设置规则的各项属性
|
||||
newRule.Name = request.Name; // 规则名称
|
||||
newRule.Description = request.Description; // 规则描述
|
||||
newRule.ApplicationName = request.ApplicationName; // 应用程序路径
|
||||
newRule.Name = request.Name; // 规则名称
|
||||
newRule.Description = request.Description; // 规则描述
|
||||
newRule.ApplicationName = request.ApplicationName; // 应用程序路径
|
||||
// 注意:INetFwRule 接口可能没有 ServiceName 属性,这里移除了对它的直接赋值
|
||||
newRule.Protocol = (int)request.Protocol; // 协议类型
|
||||
newRule.LocalPorts = request.LocalPorts; // 本地端口
|
||||
newRule.RemotePorts = request.RemotePorts; // 远程端口
|
||||
newRule.LocalAddresses = request.LocalAddresses; // 本地地址
|
||||
newRule.RemoteAddresses = request.RemoteAddresses; // 远程地址
|
||||
newRule.IcmpTypesAndCodes = request.IcmpTypesAndCodes; // ICMP类型和代码
|
||||
newRule.Direction = (NET_FW_RULE_DIRECTION_)request.Direction; // 流量方向
|
||||
newRule.Enabled = request.Enabled; // 是否启用
|
||||
newRule.Profiles = ConvertToNetFwProfiles(request.Profiles); // 适用的配置文件
|
||||
newRule.Action = (NET_FW_ACTION_)request.Action; // 规则动作
|
||||
newRule.Grouping = request.Grouping; // 规则组
|
||||
newRule.InterfaceTypes = request.InterfaceTypes; // 接口类型
|
||||
newRule.Protocol = (int)request.Protocol; // 协议类型
|
||||
newRule.LocalPorts = request.LocalPorts; // 本地端口
|
||||
newRule.RemotePorts = request.RemotePorts; // 远程端口
|
||||
newRule.LocalAddresses = request.LocalAddresses; // 本地地址
|
||||
newRule.RemoteAddresses = request.RemoteAddresses; // 远程地址
|
||||
newRule.IcmpTypesAndCodes = request.IcmpTypesAndCodes; // ICMP类型和代码
|
||||
newRule.Direction = (NET_FW_RULE_DIRECTION_)request.Direction; // 流量方向
|
||||
newRule.Enabled = request.Enabled; // 是否启用
|
||||
newRule.Profiles = ConvertToNetFwProfiles(request.Profiles); // 适用的配置文件
|
||||
newRule.Action = (NET_FW_ACTION_)request.Action; // 规则动作
|
||||
newRule.Grouping = request.Grouping; // 规则组
|
||||
newRule.InterfaceTypes = request.InterfaceTypes; // 接口类型
|
||||
|
||||
// 设置边缘遍历(用于某些特殊网络场景)
|
||||
if (request.EdgeTraversal)
|
||||
@@ -469,7 +470,7 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
{
|
||||
// 先获取组中的所有规则
|
||||
var rules = GetRulesByGroup(groupName);
|
||||
|
||||
|
||||
// 逐一删除组中的每个规则
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
@@ -620,14 +621,14 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
try
|
||||
{
|
||||
var rules = GetAllRules();
|
||||
|
||||
|
||||
// 配置JSON序列化选项,包括缩进和枚举转换
|
||||
var json = JsonSerializer.Serialize(rules, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
});
|
||||
|
||||
|
||||
System.IO.File.WriteAllText(filePath, json);
|
||||
return true;
|
||||
}
|
||||
@@ -647,7 +648,7 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
try
|
||||
{
|
||||
var json = System.IO.File.ReadAllText(filePath);
|
||||
|
||||
|
||||
// 反序列化JSON文件中的规则
|
||||
var rules = JsonSerializer.Deserialize<List<FirewallRule>>(json, new JsonSerializerOptions
|
||||
{
|
||||
@@ -700,9 +701,9 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
var rules = GetAllRules();
|
||||
var stats = new FirewallStatistics
|
||||
{
|
||||
TotalRules = rules.Count, // 总规则数
|
||||
EnabledRules = rules.Count(r => r.Enabled), // 已启用规则数
|
||||
DisabledRules = rules.Count(r => !r.Enabled) // 已禁用规则数
|
||||
TotalRules = rules.Count, // 总规则数
|
||||
EnabledRules = rules.Count(r => r.Enabled), // 已启用规则数
|
||||
DisabledRules = rules.Count(r => !r.Enabled) // 已禁用规则数
|
||||
};
|
||||
|
||||
// 按流量方向统计规则数量
|
||||
@@ -795,7 +796,7 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
public bool BlockApplication(string applicationPath)
|
||||
{
|
||||
var fileName = System.IO.Path.GetFileName(applicationPath);
|
||||
|
||||
|
||||
// 创建入站阻止规则
|
||||
var blockInbound = CreateRule(new CreateRuleRequest
|
||||
{
|
||||
@@ -1043,25 +1044,25 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
{
|
||||
return new FirewallRule
|
||||
{
|
||||
Name = rule.Name ?? string.Empty, // 规则名称
|
||||
Description = rule.Description ?? string.Empty, // 规则描述
|
||||
ApplicationName = rule.ApplicationName ?? string.Empty, // 应用程序路径
|
||||
Name = rule.Name ?? string.Empty, // 规则名称
|
||||
Description = rule.Description ?? string.Empty, // 规则描述
|
||||
ApplicationName = rule.ApplicationName ?? string.Empty, // 应用程序路径
|
||||
// ServiceName 在某些版本的 INetFwRule 中可能不存在,使用默认值
|
||||
ServiceName = string.Empty, // 服务名称
|
||||
Protocol = (ProtocolType)rule.Protocol, // 协议类型
|
||||
LocalPorts = rule.LocalPorts ?? string.Empty, // 本地端口
|
||||
RemotePorts = rule.RemotePorts ?? string.Empty, // 远程端口
|
||||
LocalAddresses = rule.LocalAddresses ?? string.Empty, // 本地地址
|
||||
RemoteAddresses = rule.RemoteAddresses ?? string.Empty, // 远程地址
|
||||
IcmpTypesAndCodes = rule.IcmpTypesAndCodes ?? string.Empty, // ICMP类型和代码
|
||||
Direction = (RuleDirection)rule.Direction, // 流量方向
|
||||
Enabled = rule.Enabled, // 是否启用
|
||||
Profiles = ConvertFromNetFwProfiles(rule.Profiles), // 适用的配置文件
|
||||
EdgeTraversal = rule.EdgeTraversal, // 边缘遍历设置
|
||||
Action = (RuleAction)rule.Action, // 规则动作
|
||||
Grouping = rule.Grouping ?? string.Empty, // 规则组
|
||||
InterfaceTypes = rule.InterfaceTypes ?? string.Empty, // 接口类型
|
||||
Interfaces = ParseInterfaces(rule.Interfaces) // 接口列表
|
||||
ServiceName = string.Empty, // 服务名称
|
||||
Protocol = (ProtocolType)rule.Protocol, // 协议类型
|
||||
LocalPorts = rule.LocalPorts ?? string.Empty, // 本地端口
|
||||
RemotePorts = rule.RemotePorts ?? string.Empty, // 远程端口
|
||||
LocalAddresses = rule.LocalAddresses ?? string.Empty, // 本地地址
|
||||
RemoteAddresses = rule.RemoteAddresses ?? string.Empty, // 远程地址
|
||||
IcmpTypesAndCodes = rule.IcmpTypesAndCodes ?? string.Empty, // ICMP类型和代码
|
||||
Direction = (RuleDirection)rule.Direction, // 流量方向
|
||||
Enabled = rule.Enabled, // 是否启用
|
||||
Profiles = ConvertFromNetFwProfiles(rule.Profiles), // 适用的配置文件
|
||||
EdgeTraversal = rule.EdgeTraversal, // 边缘遍历设置
|
||||
Action = (RuleAction)rule.Action, // 规则动作
|
||||
Grouping = rule.Grouping ?? string.Empty, // 规则组
|
||||
InterfaceTypes = rule.InterfaceTypes ?? string.Empty, // 接口类型
|
||||
Interfaces = ParseInterfaces(rule.Interfaces) // 接口列表
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1137,4 +1138,4 @@ public class WindowsFirewallApi : IFirewallApi
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
VITE_MODE='development'
|
||||
VITE_APP_API_URL='http://localhost:8268/api'
|
||||
|
||||
我想让FirewallRuleForm.vue的滚动区域,只在页面主容器内进行,比如“基础内容”很多,那么就在“基础内容”的
|
||||
子Tab中进行滚动,不能超出去,不然滚动时将会整体滚动,非常不优雅,而且你这次优化的样式我觉得还有提升空间,你可以参考MonitorView下的组件进行样式优化
|
||||
+3
-1
@@ -9,14 +9,16 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CommandManager: typeof import('./src/components/CommandManager.vue')['default']
|
||||
FirewallRuleForm: typeof import('./src/components/FirewallRuleForm.vue')['default']
|
||||
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
|
||||
IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
|
||||
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
|
||||
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
|
||||
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
|
||||
NetworkConnectionPanel2: typeof import('./src/components/NetworkConnectionPanel2.vue')['default']
|
||||
NHighlight: typeof import('naive-ui')['NHighlight']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TitleBar: typeof import('./src/components/TitleBar.vue')['default']
|
||||
|
||||
@@ -1,922 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>防火墙管理中心</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-dark: #1d4ed8;
|
||||
--secondary: #64748b;
|
||||
--success: #22c55e;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--background: #0f172a;
|
||||
--surface: #1e293b;
|
||||
--surface-light: #334155;
|
||||
--text: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--border: #475569;
|
||||
--shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: var(--surface);
|
||||
padding: 20px 30px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-icon.active {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-icon.blocked {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 12px 20px 12px 45px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.search-box::before {
|
||||
content: "🔍";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px -10px rgba(37, 99, 235, 0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--surface-light);
|
||||
}
|
||||
|
||||
.btn-secondary.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Rules Table */
|
||||
.rules-container {
|
||||
background: var(--surface);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.rules-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.rules-table th {
|
||||
background: var(--surface-light);
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.rules-table td {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(71, 85, 105, 0.3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rules-table tr:hover {
|
||||
background: rgba(37, 99, 235, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rules-table tr:hover td {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.rule-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.direction-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.direction-in {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.direction-out {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--surface);
|
||||
border-radius: 20px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid var(--border);
|
||||
animation: slideUp 0.3s;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: var(--surface-light);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--danger);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-group, .checkbox-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-item, .checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"],
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.port-range {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.service-item {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background: var(--surface-light);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h1>防火墙管理中心</h1>
|
||||
<p style="color: var(--text-secondary);">实时监控和管理网络安全规则</p>
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon active">✓</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: 600;">42</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">活动规则</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon total">🛡️</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: 600;">68</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">总规则数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon blocked">🚫</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: 600;">1,247</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">今日阻止</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="搜索规则名称、程序、端口...">
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<button class="btn btn-secondary active" onclick="filterRules('all')">全部</button>
|
||||
<button class="btn btn-secondary" onclick="filterRules('inbound')">入站</button>
|
||||
<button class="btn btn-secondary" onclick="filterRules('outbound')">出站</button>
|
||||
<button class="btn btn-primary" onclick="openModal()">
|
||||
<span>+</span> 新建规则
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules Table -->
|
||||
<div class="rules-container">
|
||||
<table class="rules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>状态</th>
|
||||
<th>方向</th>
|
||||
<th>程序</th>
|
||||
<th>协议/端口</th>
|
||||
<th>作用域</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rulesTableBody">
|
||||
<!-- Rules will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="ruleModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">新建防火墙规则</h2>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Basic Settings -->
|
||||
<div class="form-section">
|
||||
<h3>🔧 基本设置</h3>
|
||||
<div class="form-group">
|
||||
<label>规则名称</label>
|
||||
<input type="text" class="form-control" id="ruleName" placeholder="输入规则名称">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述(可选)</label>
|
||||
<input type="text" class="form-control" id="ruleDescription" placeholder="输入规则描述">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>方向</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="direction" value="inbound" checked>
|
||||
<span>入站</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="direction" value="outbound">
|
||||
<span>出站</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>操作</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="action" value="allow" checked>
|
||||
<span>允许连接</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="action" value="block">
|
||||
<span>阻止连接</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="action" value="secure">
|
||||
<span>允许安全连接</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Program Settings -->
|
||||
<div class="form-section">
|
||||
<h3>📱 程序设置</h3>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('program', 'allPrograms')">所有程序</button>
|
||||
<button class="tab" onclick="switchTab('program', 'specificProgram')">指定程序</button>
|
||||
<button class="tab" onclick="switchTab('program', 'service')">服务</button>
|
||||
</div>
|
||||
<div class="tab-content active" id="allPrograms">
|
||||
<p style="color: var(--text-secondary);">此规则适用于所有程序</p>
|
||||
</div>
|
||||
<div class="tab-content" id="specificProgram">
|
||||
<div class="form-group">
|
||||
<label>程序路径</label>
|
||||
<input type="text" class="form-control" placeholder="浏览或输入程序路径">
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" id="service">
|
||||
<div class="form-group">
|
||||
<label>选择服务</label>
|
||||
<div class="service-list">
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service1">
|
||||
<label for="service1">Windows Update</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service2">
|
||||
<label for="service2">远程桌面服务</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service3">
|
||||
<label for="service3">文件和打印机共享</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service4">
|
||||
<label for="service4">SQL Server</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service5">
|
||||
<label for="service5">IIS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocol and Ports -->
|
||||
<div class="form-section">
|
||||
<h3>🔌 协议和端口</h3>
|
||||
<div class="form-group">
|
||||
<label>协议类型</label>
|
||||
<select class="form-control" id="protocol" onchange="updatePortOptions()">
|
||||
<option value="any">任意</option>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="icmpv4">ICMPv4</option>
|
||||
<option value="icmpv6">ICMPv6</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="portSettings" style="display: none;">
|
||||
<label>端口</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="portType" value="all" checked>
|
||||
<span>所有端口</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="portType" value="specific">
|
||||
<span>特定端口</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="port-range" style="margin-top: 10px; display: none;" id="portRange">
|
||||
<input type="text" class="form-control" placeholder="端口或范围 (如: 80, 443, 1000-2000)" style="flex: 1;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scope -->
|
||||
<div class="form-section">
|
||||
<h3>🌐 作用域</h3>
|
||||
<div class="form-group">
|
||||
<label>本地IP地址</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="localIP" value="any" checked>
|
||||
<span>任何IP地址</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="localIP" value="specific">
|
||||
<span>指定IP地址</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>远程IP地址</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="remoteIP" value="any" checked>
|
||||
<span>任何IP地址</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="remoteIP" value="specific">
|
||||
<span>指定IP地址</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced -->
|
||||
<div class="form-section">
|
||||
<h3>⚙️ 高级设置</h3>
|
||||
<div class="form-group">
|
||||
<label>配置文件</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" checked>
|
||||
<span>域</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" checked>
|
||||
<span>专用</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" checked>
|
||||
<span>公用</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>用户和组</label>
|
||||
<button class="btn btn-secondary">选择用户或组...</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveRule()">保存规则</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Sample data
|
||||
const rules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Chrome 浏览器",
|
||||
status: "enabled",
|
||||
direction: "outbound",
|
||||
program: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
protocol: "TCP",
|
||||
port: "80, 443",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "远程桌面 - 用户模式 (TCP-In)",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "系统",
|
||||
protocol: "TCP",
|
||||
port: "3389",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "文件和打印机共享 (SMB-In)",
|
||||
status: "disabled",
|
||||
direction: "inbound",
|
||||
program: "系统",
|
||||
protocol: "TCP",
|
||||
port: "445",
|
||||
scope: "本地子网",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Windows Update",
|
||||
status: "enabled",
|
||||
direction: "outbound",
|
||||
program: "svchost.exe",
|
||||
protocol: "TCP/UDP",
|
||||
port: "80, 443",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "SQL Server",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "sqlservr.exe",
|
||||
protocol: "TCP",
|
||||
port: "1433",
|
||||
scope: "指定IP",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "阻止可疑端口",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "所有程序",
|
||||
protocol: "TCP",
|
||||
port: "135, 139, 445",
|
||||
scope: "任意",
|
||||
action: "阻止"
|
||||
}
|
||||
];
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
renderRules(rules);
|
||||
}
|
||||
|
||||
// Render rules table
|
||||
function renderRules(rulesToRender) {
|
||||
const tbody = document.getElementById('rulesTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
rulesToRender.forEach(rule => {
|
||||
const row = document.createElement('tr');
|
||||
row.onclick = () => editRule(rule);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<div class="rule-name">
|
||||
<div class="rule-icon">${rule.direction === 'inbound' ? '↓' : '↑'}</div>
|
||||
<span>${rule.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-${rule.status}">
|
||||
${rule.status === 'enabled' ? '已启用' : '已禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="direction-badge direction-${rule.direction === 'inbound' ? 'in' : 'out'}">
|
||||
${rule.direction === 'inbound' ? '入站' : '出站'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${rule.program.split('\\').pop()}</td>
|
||||
<td>${rule.protocol}${rule.port !== '任意' ? ': ' + rule.port : ''}</td>
|
||||
<td>${rule.scope}</td>
|
||||
<td>${rule.action}</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter rules
|
||||
function filterRules(type) {
|
||||
const buttons = document.querySelectorAll('.filter-buttons .btn-secondary');
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
let filtered = rules;
|
||||
if (type === 'inbound') {
|
||||
filtered = rules.filter(r => r.direction === 'inbound');
|
||||
} else if (type === 'outbound') {
|
||||
filtered = rules.filter(r => r.direction === 'outbound');
|
||||
}
|
||||
|
||||
renderRules(filtered);
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
document.querySelector('.search-box input').addEventListener('input', function(e
|
||||
@@ -1,1497 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>防火墙管理中心</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-dark: #1d4ed8;
|
||||
--secondary: #64748b;
|
||||
--success: #22c55e;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--background: #0f172a;
|
||||
--surface: #1e293b;
|
||||
--surface-light: #334155;
|
||||
--text: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--border: #475569;
|
||||
--shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: var(--surface);
|
||||
padding: 20px 30px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--primary), var(--success), var(--warning));
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: rgba(37, 99, 235, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px -8px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stat-icon.active {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--success);
|
||||
box-shadow: 0 0 20px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
color: var(--primary);
|
||||
box-shadow: 0 0 20px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.stat-icon.blocked {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 12px 20px 12px 45px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.search-box::before {
|
||||
content: "🔍";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--surface-light);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
/* Rules Table */
|
||||
.rules-container {
|
||||
background: var(--surface);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.rules-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.rules-table th {
|
||||
background: var(--surface-light);
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.rules-table td {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(71, 85, 105, 0.3);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rules-table tr {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.rules-table tr:hover {
|
||||
background: linear-gradient(90deg, rgba(37, 99, 235, 0.05), rgba(37, 99, 235, 0.1));
|
||||
cursor: pointer;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.rules-table tr:hover td {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.rule-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(37, 99, 235, 0.2));
|
||||
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--success);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.direction-badge {
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.direction-in {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
color: #60a5fa;
|
||||
border-color: rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.direction-out {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fbbf24;
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn.edit {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--surface);
|
||||
border-radius: 20px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid var(--border);
|
||||
animation: slideUp 0.3s;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, var(--surface), var(--surface-light));
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #60a5fa, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: var(--surface-light);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--danger);
|
||||
transform: scale(1.1) rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 25px;
|
||||
background: rgba(37, 99, 235, 0.03);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: var(--background);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-group, .checkbox-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-item, .checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.radio-item:hover, .checkbox-item:hover {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"],
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary);
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
background: var(--background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.port-range {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.service-item {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background: var(--surface-light);
|
||||
border-color: var(--primary);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
background: rgba(37, 99, 235, 0.02);
|
||||
}
|
||||
|
||||
/* Notification */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 2000;
|
||||
animation: slideInRight 0.3s;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.notification.info {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--surface);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-light);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rules-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rules-table th,
|
||||
.rules-table td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h1>防火墙管理中心</h1>
|
||||
<p style="color: var(--text-secondary);">实时监控和管理网络安全规则</p>
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon active">✓</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: 600;" id="activeRules">42</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">活动规则</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon total">🛡️</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: 600;" id="totalRules">68</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">总规则数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon blocked">🚫</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: 600;" id="blockedToday">1,247</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">今日阻止</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchInput" placeholder="搜索规则名称、程序、端口...">
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<button class="btn btn-secondary active" onclick="filterRules('all')">全部</button>
|
||||
<button class="btn btn-secondary" onclick="filterRules('inbound')">入站</button>
|
||||
<button class="btn btn-secondary" onclick="filterRules('outbound')">出站</button>
|
||||
<button class="btn btn-primary" onclick="openModal()">
|
||||
<span>+</span> 新建规则
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules Table -->
|
||||
<div class="rules-container">
|
||||
<table class="rules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>状态</th>
|
||||
<th>方向</th>
|
||||
<th>程序</th>
|
||||
<th>协议/端口</th>
|
||||
<th>作用域</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rulesTableBody">
|
||||
<!-- Rules will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="ruleModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">新建防火墙规则</h2>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="ruleForm">
|
||||
<!-- Basic Settings -->
|
||||
<div class="form-section">
|
||||
<h3>🔧 基本设置</h3>
|
||||
<div class="form-group">
|
||||
<label>规则名称 *</label>
|
||||
<input type="text" class="form-control" id="ruleName" placeholder="输入规则名称" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述(可选)</label>
|
||||
<input type="text" class="form-control" id="ruleDescription" placeholder="输入规则描述">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>方向</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="direction" value="inbound" checked>
|
||||
<span>入站</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="direction" value="outbound">
|
||||
<span>出站</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>操作</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="action" value="allow" checked>
|
||||
<span>允许连接</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="action" value="block">
|
||||
<span>阻止连接</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="action" value="secure">
|
||||
<span>允许安全连接</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Program Settings -->
|
||||
<div class="form-section">
|
||||
<h3>📱 程序设置</h3>
|
||||
<div class="tabs">
|
||||
<button type="button" class="tab active" onclick="switchTab('program', 'allPrograms')">所有程序</button>
|
||||
<button type="button" class="tab" onclick="switchTab('program', 'specificProgram')">指定程序</button>
|
||||
<button type="button" class="tab" onclick="switchTab('program', 'service')">服务</button>
|
||||
</div>
|
||||
<div class="tab-content active" id="allPrograms">
|
||||
<p style="color: var(--text-secondary);">此规则适用于所有程序</p>
|
||||
</div>
|
||||
<div class="tab-content" id="specificProgram">
|
||||
<div class="form-group">
|
||||
<label>程序路径</label>
|
||||
<input type="text" class="form-control" id="programPath" placeholder="浏览或输入程序路径">
|
||||
<button type="button" class="btn btn-secondary" style="margin-top: 10px;" onclick="browseProgramPath()">浏览...</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" id="service">
|
||||
<div class="form-group">
|
||||
<label>选择服务</label>
|
||||
<div class="service-list">
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service1" value="windows_update">
|
||||
<label for="service1">Windows Update</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service2" value="remote_desktop">
|
||||
<label for="service2">远程桌面服务</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service3" value="file_sharing">
|
||||
<label for="service3">文件和打印机共享</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service4" value="sql_server">
|
||||
<label for="service4">SQL Server</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<input type="checkbox" id="service5" value="iis">
|
||||
<label for="service5">IIS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocol and Ports -->
|
||||
<div class="form-section">
|
||||
<h3>🔌 协议和端口</h3>
|
||||
<div class="form-group">
|
||||
<label>协议类型</label>
|
||||
<select class="form-control" id="protocol" onchange="updatePortOptions()">
|
||||
<option value="any">任意</option>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="icmpv4">ICMPv4</option>
|
||||
<option value="icmpv6">ICMPv6</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="portSettings" style="display: none;">
|
||||
<label>端口</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="portType" value="all" checked onchange="togglePortRange()">
|
||||
<span>所有端口</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="portType" value="specific" onchange="togglePortRange()">
|
||||
<span>特定端口</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="port-range" style="margin-top: 10px; display: none;" id="portRange">
|
||||
<input type="text" class="form-control" id="portInput" placeholder="端口或范围 (如: 80, 443, 1000-2000)" style="flex: 1;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scope -->
|
||||
<div class="form-section">
|
||||
<h3>🌐 作用域</h3>
|
||||
<div class="form-group">
|
||||
<label>本地IP地址</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="localIP" value="any" checked onchange="toggleLocalIP()">
|
||||
<span>任何IP地址</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="localIP" value="specific" onchange="toggleLocalIP()">
|
||||
<span>指定IP地址</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="localIPInput" style="margin-top: 10px; display: none;">
|
||||
<input type="text" class="form-control" placeholder="输入IP地址或范围">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>远程IP地址</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="remoteIP" value="any" checked onchange="toggleRemoteIP()">
|
||||
<span>任何IP地址</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="remoteIP" value="specific" onchange="toggleRemoteIP()">
|
||||
<span>指定IP地址</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="remoteIPInput" style="margin-top: 10px; display: none;">
|
||||
<input type="text" class="form-control" placeholder="输入IP地址或范围">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced -->
|
||||
<div class="form-section">
|
||||
<h3>⚙️ 高级设置</h3>
|
||||
<div class="form-group">
|
||||
<label>配置文件</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" name="profile" value="domain" checked>
|
||||
<span>域</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" name="profile" value="private" checked>
|
||||
<span>专用</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" name="profile" value="public" checked>
|
||||
<span>公用</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>启用规则</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="enableRule" checked>
|
||||
<span>创建后立即启用此规则</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveRule()">保存规则</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables
|
||||
let rules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Chrome 浏览器",
|
||||
status: "enabled",
|
||||
direction: "outbound",
|
||||
program: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
protocol: "TCP",
|
||||
port: "80, 443",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "远程桌面 - 用户模式 (TCP-In)",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "系统",
|
||||
protocol: "TCP",
|
||||
port: "3389",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "文件和打印机共享 (SMB-In)",
|
||||
status: "disabled",
|
||||
direction: "inbound",
|
||||
program: "系统",
|
||||
protocol: "TCP",
|
||||
port: "445",
|
||||
scope: "本地子网",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Windows Update",
|
||||
status: "enabled",
|
||||
direction: "outbound",
|
||||
program: "svchost.exe",
|
||||
protocol: "TCP/UDP",
|
||||
port: "80, 443",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "SQL Server",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "sqlservr.exe",
|
||||
protocol: "TCP",
|
||||
port: "1433",
|
||||
scope: "指定IP",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "阻止可疑端口",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "所有程序",
|
||||
protocol: "TCP",
|
||||
port: "135, 139, 445",
|
||||
scope: "任意",
|
||||
action: "阻止"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "SSH 服务",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "sshd.exe",
|
||||
protocol: "TCP",
|
||||
port: "22",
|
||||
scope: "指定IP",
|
||||
action: "允许"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "HTTP 服务器",
|
||||
status: "enabled",
|
||||
direction: "inbound",
|
||||
program: "httpd.exe",
|
||||
protocol: "TCP",
|
||||
port: "80",
|
||||
scope: "任意",
|
||||
action: "允许"
|
||||
}
|
||||
];
|
||||
|
||||
let currentFilter = 'all';
|
||||
let editingRuleId = null;
|
||||
|
||||
// Initialize application
|
||||
function init() {
|
||||
renderRules(rules);
|
||||
updateStats();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const filteredRules = rules.filter(rule => {
|
||||
const matchesSearch = rule.name.toLowerCase().includes(searchTerm) ||
|
||||
rule.program.toLowerCase().includes(searchTerm) ||
|
||||
rule.port.toLowerCase().includes(searchTerm) ||
|
||||
rule.protocol.toLowerCase().includes(searchTerm);
|
||||
|
||||
const matchesFilter = currentFilter === 'all' || rule.direction === currentFilter;
|
||||
|
||||
return matchesSearch && matchesFilter;
|
||||
});
|
||||
renderRules(filteredRules);
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('ruleModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent form submission
|
||||
document.getElementById('ruleForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveRule();
|
||||
});
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
function updateStats() {
|
||||
const activeRules = rules.filter(rule => rule.status === 'enabled').length;
|
||||
const totalRules = rules.length;
|
||||
const blockedToday = Math.floor(Math.random() * 2000) + 1000; // Simulated data
|
||||
|
||||
document.getElementById('activeRules').textContent = activeRules;
|
||||
document.getElementById('totalRules').textContent = totalRules;
|
||||
document.getElementById('blockedToday').textContent = blockedToday.toLocaleString();
|
||||
}
|
||||
|
||||
// Render rules table
|
||||
function renderRules(rulesToRender) {
|
||||
const tbody = document.getElementById('rulesTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (rulesToRender.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td colspan="7" style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
||||
没有找到匹配的规则
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
return;
|
||||
}
|
||||
|
||||
rulesToRender.forEach(rule => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<div class="rule-name">
|
||||
<div class="rule-icon">${rule.direction === 'inbound' ? '↓' : '↑'}</div>
|
||||
<span>${rule.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-${rule.status}">
|
||||
${rule.status === 'enabled' ? '已启用' : '已禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="direction-badge direction-${rule.direction === 'inbound' ? 'in' : 'out'}">
|
||||
${rule.direction === 'inbound' ? '入站' : '出站'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${rule.program.split('\\').pop()}</td>
|
||||
<td>${rule.protocol}${rule.port !== '任意' ? ': ' + rule.port : ''}</td>
|
||||
<td>${rule.scope}</td>
|
||||
<td>
|
||||
<div class="action-cell">
|
||||
<button class="action-btn edit" onclick="editRule(${rule.id})" title="编辑">
|
||||
✏️
|
||||
</button>
|
||||
<button class="action-btn delete" onclick="deleteRule(${rule.id})" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter rules
|
||||
function filterRules(type) {
|
||||
currentFilter = type;
|
||||
|
||||
// Update button states
|
||||
const buttons = document.querySelectorAll('.filter-buttons .btn-secondary');
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Apply filter
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
let filtered = rules;
|
||||
|
||||
if (type !== 'all') {
|
||||
filtered = rules.filter(r => r.direction === type);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(rule => {
|
||||
return rule.name.toLowerCase().includes(searchTerm) ||
|
||||
rule.program.toLowerCase().includes(searchTerm) ||
|
||||
rule.port.toLowerCase().includes(searchTerm) ||
|
||||
rule.protocol.toLowerCase().includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
renderRules(filtered);
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
function openModal(ruleId = null) {
|
||||
editingRuleId = ruleId;
|
||||
const modal = document.getElementById('ruleModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
|
||||
if (ruleId) {
|
||||
const rule = rules.find(r => r.id === ruleId);
|
||||
modalTitle.textContent = '编辑防火墙规则';
|
||||
populateForm(rule);
|
||||
} else {
|
||||
modalTitle.textContent = '新建防火墙规则';
|
||||
resetForm();
|
||||
}
|
||||
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('ruleModal');
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
editingRuleId = null;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById('ruleForm').reset();
|
||||
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.querySelector('.tab').classList.add('active');
|
||||
document.querySelector('.tab-content').classList.add('active');
|
||||
document.getElementById('portSettings').style.display = 'none';
|
||||
document.getElementById('portRange').style.display = 'none';
|
||||
}
|
||||
|
||||
function populateForm(rule) {
|
||||
document.getElementById('ruleName').value = rule.name;
|
||||
document.querySelector(`input[name="direction"][value="${rule.direction}"]`).checked = true;
|
||||
document.querySelector(`input[name="action"][value="${rule.action.toLowerCase()}"]`).checked = true;
|
||||
document.getElementById('enableRule').checked = rule.status === 'enabled';
|
||||
|
||||
// Set protocol and ports
|
||||
if (rule.protocol !== '任意') {
|
||||
const protocolSelect = document.getElementById('protocol');
|
||||
if (rule.protocol.includes('/')) {
|
||||
protocolSelect.value = 'custom';
|
||||
} else {
|
||||
protocolSelect.value = rule.protocol.toLowerCase();
|
||||
}
|
||||
updatePortOptions();
|
||||
|
||||
if (rule.port !== '任意') {
|
||||
document.querySelector('input[name="portType"][value="specific"]').checked = true;
|
||||
togglePortRange();
|
||||
document.getElementById('portInput').value = rule.port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
function switchTab(group, tabId) {
|
||||
// Remove active class from all tabs in this group
|
||||
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked tab and its content
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
}
|
||||
|
||||
// Protocol and port handling
|
||||
function updatePortOptions() {
|
||||
const protocol = document.getElementById('protocol').value;
|
||||
const portSettings = document.getElementById('portSettings');
|
||||
|
||||
if (protocol === 'tcp' || protocol === 'udp' || protocol === 'custom') {
|
||||
portSettings.style.display = 'block';
|
||||
} else {
|
||||
portSettings.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function togglePortRange() {
|
||||
const portType = document.querySelector('input[name="portType"]:checked').value;
|
||||
const portRange = document.getElementById('portRange');
|
||||
|
||||
if (portType === 'specific') {
|
||||
portRange.style.display = 'flex';
|
||||
} else {
|
||||
portRange.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLocalIP() {
|
||||
const ipType = document.querySelector('input[name="localIP"]:checked').value;
|
||||
const ipInput = document.getElementById('localIPInput');
|
||||
|
||||
if (ipType === 'specific') {
|
||||
ipInput.style.display = 'block';
|
||||
} else {
|
||||
ipInput.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRemoteIP() {
|
||||
const ipType = document.querySelector('input[name="remoteIP"]:checked').value;
|
||||
const ipInput = document.getElementById('remoteIPInput');
|
||||
|
||||
if (ipType === 'specific') {
|
||||
ipInput.style.display = 'block';
|
||||
} else {
|
||||
ipInput.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Rule management
|
||||
function saveRule() {
|
||||
const form = document.getElementById('ruleForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Validate required fields
|
||||
const ruleName = document.getElementById('ruleName').value.trim();
|
||||
if (!ruleName) {
|
||||
showNotification('请输入规则名称', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create rule object
|
||||
const rule = {
|
||||
id: editingRuleId || Date.now(),
|
||||
name: ruleName,
|
||||
description: document.getElementById('ruleDescription').value,
|
||||
status: document.getElementById('enableRule').checked ? 'enabled' : 'disabled',
|
||||
direction: document.querySelector('input[name="direction"]:checked').value,
|
||||
action: document.querySelector('input[name="action"]:checked').value,
|
||||
program: getProgramValue(),
|
||||
protocol: getProtocolValue(),
|
||||
port: getPortValue(),
|
||||
scope: getScopeValue()
|
||||
};
|
||||
|
||||
if (editingRuleId) {
|
||||
// Update existing rule
|
||||
const index = rules.findIndex(r => r.id === editingRuleId);
|
||||
if (index !== -1) {
|
||||
rules[index] = rule;
|
||||
showNotification('规则已更新', 'success');
|
||||
}
|
||||
} else {
|
||||
// Add new rule
|
||||
rules.push(rule);
|
||||
showNotification('规则已创建', 'success');
|
||||
}
|
||||
|
||||
renderRules(rules);
|
||||
updateStats();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function editRule(ruleId) {
|
||||
openModal(ruleId);
|
||||
}
|
||||
|
||||
function deleteRule(ruleId) {
|
||||
if (confirm('确定要删除这个规则吗?')) {
|
||||
rules = rules.filter(r => r.id !== ruleId);
|
||||
renderRules(rules);
|
||||
updateStats();
|
||||
showNotification('规则已删除', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for form data
|
||||
function getProgramValue() {
|
||||
const activeTab = document.querySelector('.tab.active').textContent;
|
||||
if (activeTab === '所有程序') {
|
||||
return '所有程序';
|
||||
} else if (activeTab === '指定程序') {
|
||||
return document.getElementById('programPath').value || '指定程序';
|
||||
} else {
|
||||
const selectedServices = Array.from(document.querySelectorAll('#service input[type="checkbox"]:checked'))
|
||||
.map(cb => cb.nextElementSibling.textContent).join(', ');
|
||||
return selectedServices || '服务';
|
||||
}
|
||||
}
|
||||
|
||||
function getProtocolValue() {
|
||||
const protocol = document.getElementById('protocol').value;
|
||||
return protocol === 'any' ? '任意' : protocol.toUpperCase();
|
||||
}
|
||||
|
||||
function getPortValue() {
|
||||
const protocol = document.getElementById('protocol').value;
|
||||
if (protocol === 'any' || protocol === 'icmpv4' || protocol === 'icmpv6') {
|
||||
return '任意';
|
||||
}
|
||||
|
||||
const portType = document.querySelector('input[name="portType"]:checked');
|
||||
if (!portType || portType.value === 'all') {
|
||||
return '任意';
|
||||
}
|
||||
|
||||
return document.getElementById('portInput').value || '任意';
|
||||
}
|
||||
|
||||
function getScopeValue() {
|
||||
const localIP = document.querySelector('input[name="localIP"]:checked').value;
|
||||
const remoteIP = document.querySelector('input[name="remoteIP"]:checked').value;
|
||||
|
||||
if (localIP === 'any' && remoteIP === 'any') {
|
||||
return '任意';
|
||||
} else if (localIP === 'specific' || remoteIP === 'specific') {
|
||||
return '指定IP';
|
||||
} else {
|
||||
return '本地子网';
|
||||
}
|
||||
}
|
||||
|
||||
function browseProgramPath() {
|
||||
// Simulate file browser (in real implementation, this would open a file dialog)
|
||||
const path = prompt('请输入程序路径:', 'C:\\Program Files\\');
|
||||
if (path) {
|
||||
document.getElementById('programPath').value = path;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification system
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Initialize application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,1205 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>防火墙管理看板</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 25px;
|
||||
padding: 10px 20px;
|
||||
min-width: 300px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #333;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 15px 20px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #f8f9ff;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-badge {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-allow {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.action-block {
|
||||
background: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.direction-badge {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.direction-inbound {
|
||||
background: #ffeaa7;
|
||||
color: #d68910;
|
||||
}
|
||||
|
||||
.direction-outbound {
|
||||
background: #fab1a0;
|
||||
color: #e17055;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(0.8);
|
||||
animation: modalScale 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes modalScale {
|
||||
to { transform: scale(1); }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 25px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #667eea;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid #eee;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: #138496;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h1>🛡️ 防火墙管理中心</h1>
|
||||
<p>监控和管理网络安全策略,保护您的系统免受恶意攻击</p>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">127</div>
|
||||
<div class="stat-label">活跃规则</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">89</div>
|
||||
<div class="stat-label">入站规则</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">38</div>
|
||||
<div class="stat-label">出站规则</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">15</div>
|
||||
<div class="stat-label">阻止的连接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div class="search-box">
|
||||
<span>🔍</span>
|
||||
<input type="text" placeholder="搜索防火墙规则..." id="searchInput">
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick="openModal('add')">
|
||||
➕ 新建规则
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="refreshRules()">
|
||||
🔄 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules Table -->
|
||||
<div class="table-container">
|
||||
<table class="table" id="rulesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>方向</th>
|
||||
<th>操作</th>
|
||||
<th>协议</th>
|
||||
<th>端口</th>
|
||||
<th>程序</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rulesTableBody">
|
||||
<!-- 动态生成的内容 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="ruleModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="modalTitle">新建防火墙规则</h2>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('general')">常规</button>
|
||||
<button class="tab" onclick="switchTab('protocols')">协议和端口</button>
|
||||
<button class="tab" onclick="switchTab('programs')">程序和服务</button>
|
||||
<button class="tab" onclick="switchTab('advanced')">高级</button>
|
||||
</div>
|
||||
|
||||
<form id="ruleForm">
|
||||
<!-- General Tab -->
|
||||
<div class="tab-content active" id="general">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">规则名称</label>
|
||||
<input type="text" class="form-input" name="name" placeholder="输入规则名称" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">方向</label>
|
||||
<select class="form-select" name="direction" required>
|
||||
<option value="inbound">入站</option>
|
||||
<option value="outbound">出站</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">操作</label>
|
||||
<select class="form-select" name="action" required>
|
||||
<option value="allow">允许连接</option>
|
||||
<option value="block">阻止连接</option>
|
||||
<option value="allow_secure">只有安全连接才允许</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">配置文件</label>
|
||||
<select class="form-select" name="profile">
|
||||
<option value="all">所有配置文件</option>
|
||||
<option value="domain">域</option>
|
||||
<option value="private">专用</option>
|
||||
<option value="public">公用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea class="form-textarea" name="description" placeholder="输入规则描述(可选)"></textarea>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" name="enabled" id="enabled" checked>
|
||||
<label for="enabled">启用此规则</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocols Tab -->
|
||||
<div class="tab-content" id="protocols">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">协议类型</label>
|
||||
<select class="form-select" name="protocol" onchange="togglePortSettings()">
|
||||
<option value="any">任何</option>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="icmp">ICMPv4</option>
|
||||
<option value="icmpv6">ICMPv6</option>
|
||||
<option value="gre">GRE</option>
|
||||
<option value="ah">AH</option>
|
||||
<option value="esp">ESP</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="customProtocolGroup" style="display: none;">
|
||||
<label class="form-label">自定义协议号</label>
|
||||
<input type="number" class="form-input" name="customProtocol" placeholder="0-255">
|
||||
</div>
|
||||
<div class="form-group" id="localPortGroup">
|
||||
<label class="form-label">本地端口</label>
|
||||
<select class="form-select" name="localPortType" onchange="toggleLocalPortInput()">
|
||||
<option value="all">所有端口</option>
|
||||
<option value="specific">特定端口</option>
|
||||
<option value="range">端口范围</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="localPortInputGroup" style="display: none;">
|
||||
<label class="form-label">端口值</label>
|
||||
<input type="text" class="form-input" name="localPort" placeholder="例如: 80, 443 或 8000-9000">
|
||||
</div>
|
||||
<div class="form-group" id="remotePortGroup">
|
||||
<label class="form-label">远程端口</label>
|
||||
<select class="form-select" name="remotePortType" onchange="toggleRemotePortInput()">
|
||||
<option value="all">所有端口</option>
|
||||
<option value="specific">特定端口</option>
|
||||
<option value="range">端口范围</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="remotePortInputGroup" style="display: none;">
|
||||
<label class="form-label">端口值</label>
|
||||
<input type="text" class="form-input" name="remotePort" placeholder="例如: 80, 443 或 8000-9000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Programs Tab -->
|
||||
<div class="tab-content" id="programs">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">程序类型</label>
|
||||
<select class="form-select" name="programType" onchange="toggleProgramSettings()">
|
||||
<option value="all">所有程序</option>
|
||||
<option value="specific">特定程序</option>
|
||||
<option value="service">服务</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="programPathGroup" style="display: none;">
|
||||
<label class="form-label">程序路径</label>
|
||||
<input type="text" class="form-input" name="programPath" placeholder="选择程序路径">
|
||||
<button type="button" class="btn btn-secondary btn-small" style="margin-top: 10px;" onclick="browseProgramPath()">浏览...</button>
|
||||
</div>
|
||||
<div class="form-group" id="serviceGroup" style="display: none;">
|
||||
<label class="form-label">应用于下列服务</label>
|
||||
<select class="form-select" name="serviceType" onchange="toggleServiceOptions()">
|
||||
<option value="all">所有服务</option>
|
||||
<option value="specific">特定服务</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="serviceListGroup" style="display: none;">
|
||||
<label class="form-label">选择服务</label>
|
||||
<select class="form-select" name="service" multiple size="5">
|
||||
<option value="http">HTTP 服务</option>
|
||||
<option value="https">HTTPS 服务</option>
|
||||
<option value="ftp">FTP 服务</option>
|
||||
<option value="ssh">SSH 服务</option>
|
||||
<option value="telnet">Telnet 服务</option>
|
||||
<option value="smtp">SMTP 服务</option>
|
||||
<option value="pop3">POP3 服务</option>
|
||||
<option value="imap">IMAP 服务</option>
|
||||
<option value="dns">DNS 服务</option>
|
||||
<option value="dhcp">DHCP 服务</option>
|
||||
<option value="snmp">SNMP 服务</option>
|
||||
<option value="ldap">LDAP 服务</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Tab -->
|
||||
<div class="tab-content" id="advanced">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">本地IP地址</label>
|
||||
<select class="form-select" name="localIPType" onchange="toggleLocalIPInput()">
|
||||
<option value="any">任何IP地址</option>
|
||||
<option value="specific">特定IP地址</option>
|
||||
<option value="range">IP地址范围</option>
|
||||
<option value="subnet">子网</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="localIPInputGroup" style="display: none;">
|
||||
<label class="form-label">IP地址</label>
|
||||
<input type="text" class="form-input" name="localIP" placeholder="例如: 192.168.1.100 或 192.168.1.0/24">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">远程IP地址</label>
|
||||
<select class="form-select" name="remoteIPType" onchange="toggleRemoteIPInput()">
|
||||
<option value="any">任何IP地址</option>
|
||||
<option value="specific">特定IP地址</option>
|
||||
<option value="range">IP地址范围</option>
|
||||
<option value="subnet">子网</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="remoteIPInputGroup" style="display: none;">
|
||||
<label class="form-label">IP地址</label>
|
||||
<input type="text" class="form-input" name="remoteIP" placeholder="例如: 192.168.1.100 或 192.168.1.0/24">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">用户</label>
|
||||
<select class="form-select" name="userType" onchange="toggleUserInput()">
|
||||
<option value="any">任何用户</option>
|
||||
<option value="specific">特定用户和组</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="userInputGroup" style="display: none;">
|
||||
<label class="form-label">用户和组</label>
|
||||
<select class="form-select" name="users" multiple size="4">
|
||||
<option value="administrators">Administrators</option>
|
||||
<option value="users">Users</option>
|
||||
<option value="guests">Guests</option>
|
||||
<option value="everyone">Everyone</option>
|
||||
<option value="authenticated_users">Authenticated Users</option>
|
||||
<option value="network_service">Network Service</option>
|
||||
<option value="local_service">Local Service</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">接口类型</label>
|
||||
<select class="form-select" name="interfaceType">
|
||||
<option value="all">所有接口类型</option>
|
||||
<option value="lan">局域网</option>
|
||||
<option value="wan">广域网</option>
|
||||
<option value="wireless">无线</option>
|
||||
<option value="remote_access">远程访问</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">边缘遍历</label>
|
||||
<select class="form-select" name="edgeTraversal">
|
||||
<option value="block">阻止边缘遍历</option>
|
||||
<option value="allow">允许边缘遍历</option>
|
||||
<option value="defer_to_user">延迟到用户</option>
|
||||
<option value="defer_to_app">延迟到应用程序</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveRule()">保存规则</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 模拟数据
|
||||
let firewallRules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Core Networking - DNS (UDP-Out)",
|
||||
direction: "outbound",
|
||||
action: "allow",
|
||||
protocol: "UDP",
|
||||
localPort: "Any",
|
||||
remotePort: "53",
|
||||
program: "%SystemRoot%\\system32\\svchost.exe",
|
||||
service: "DNS Client",
|
||||
enabled: true,
|
||||
profile: "all",
|
||||
localIP: "Any",
|
||||
remoteIP: "Any",
|
||||
users: "Any",
|
||||
description: "允许 DNS 查询出站连接"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Windows Remote Management (HTTP-In)",
|
||||
direction: "inbound",
|
||||
action: "allow",
|
||||
protocol: "TCP",
|
||||
localPort: "5985",
|
||||
remotePort: "Any",
|
||||
program: "System",
|
||||
service: "WinRM",
|
||||
enabled: true,
|
||||
profile: "domain",
|
||||
localIP: "Any",
|
||||
remoteIP: "Any",
|
||||
users: "Administrators",
|
||||
description: "允许 WinRM HTTP 入站连接"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "File and Printer Sharing (SMB-In)",
|
||||
direction: "inbound",
|
||||
action: "allow",
|
||||
protocol: "TCP",
|
||||
localPort: "445",
|
||||
remotePort: "Any",
|
||||
program: "System",
|
||||
service: "Server",
|
||||
enabled: false,
|
||||
profile: "private",
|
||||
localIP: "Any",
|
||||
remoteIP: "LocalSubnet",
|
||||
users: "Any",
|
||||
description: "允许文件和打印机共享"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Block Malicious Traffic",
|
||||
direction: "inbound",
|
||||
action: "block",
|
||||
protocol: "Any",
|
||||
localPort: "Any",
|
||||
remotePort: "Any",
|
||||
program: "Any",
|
||||
service: "Any",
|
||||
enabled: true,
|
||||
profile: "all",
|
||||
localIP: "Any",
|
||||
remoteIP: "192.168.100.0/24",
|
||||
users: "Any",
|
||||
description: "阻止来自可疑网段的所有流量"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Web Server (HTTP)",
|
||||
direction: "inbound",
|
||||
action: "allow",
|
||||
protocol: "TCP",
|
||||
localPort: "80",
|
||||
remotePort: "Any",
|
||||
program: "C:\\inetpub\\wwwroot\\httpd.exe",
|
||||
service: "HTTP",
|
||||
enabled: true,
|
||||
profile: "all",
|
||||
localIP: "Any",
|
||||
remoteIP: "Any",
|
||||
users: "Any",
|
||||
description: "允许 Web 服务器 HTTP 连接"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Secure Web Server (HTTPS)",
|
||||
direction: "inbound",
|
||||
action: "allow",
|
||||
protocol: "TCP",
|
||||
localPort: "443",
|
||||
remotePort: "Any",
|
||||
program: "C:\\inetpub\\wwwroot\\httpd.exe",
|
||||
service: "HTTPS",
|
||||
enabled: true,
|
||||
profile: "all",
|
||||
localIP: "Any",
|
||||
remoteIP: "Any",
|
||||
users: "Any",
|
||||
description: "允许 Web 服务器 HTTPS 连接"
|
||||
}
|
||||
];
|
||||
|
||||
let currentRule = null;
|
||||
let isEditMode = false;
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
renderRulesTable();
|
||||
setupSearchFilter();
|
||||
});
|
||||
|
||||
// 渲染规则表格
|
||||
function renderRulesTable(rules = firewallRules) {
|
||||
const tbody = document.getElementById('rulesTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
rules.forEach(rule => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td><strong>${rule.name}</strong></td>
|
||||
<td><span class="direction-badge direction-${rule.direction}">${rule.direction === 'inbound' ? '入站' : '出站'}</span></td>
|
||||
<td><span class="action-badge action-${rule.action}">${getActionText(rule.action)}</span></td>
|
||||
<td>${rule.protocol}</td>
|
||||
<td>${rule.localPort}</td>
|
||||
<td>${rule.program.includes('\\') ? rule.program.split('\\').pop() : rule.program}</td>
|
||||
<td><span class="status-badge status-${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? '启用' : '禁用'}</span></td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-edit btn-small" onclick="editRule(${rule.id})">编辑</button>
|
||||
<button class="btn btn-delete btn-small" onclick="deleteRule(${rule.id})">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取操作文本
|
||||
function getActionText(action) {
|
||||
switch(action) {
|
||||
case 'allow': return '允许';
|
||||
case 'block': return '阻止';
|
||||
case 'allow_secure': return '安全允许';
|
||||
default: return action;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置搜索过滤
|
||||
function setupSearchFilter() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const filteredRules = firewallRules.filter(rule =>
|
||||
rule.name.toLowerCase().includes(searchTerm) ||
|
||||
rule.program.toLowerCase().includes(searchTerm) ||
|
||||
rule.protocol.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
renderRulesTable(filteredRules);
|
||||
});
|
||||
}
|
||||
|
||||
// 打开模态框
|
||||
function openModal(mode, ruleId = null) {
|
||||
const modal = document.getElementById('ruleModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
|
||||
isEditMode = mode === 'edit';
|
||||
|
||||
if (isEditMode && ruleId) {
|
||||
currentRule = firewallRules.find(rule => rule.id === ruleId);
|
||||
modalTitle.textContent = '编辑防火墙规则';
|
||||
populateForm(currentRule);
|
||||
} else {
|
||||
currentRule = null;
|
||||
modalTitle.textContent = '新建防火墙规则';
|
||||
resetForm();
|
||||
}
|
||||
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('ruleModal');
|
||||
modal.classList.remove('show');
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
document.getElementById('ruleForm').reset();
|
||||
document.querySelector('input[name="enabled"]').checked = true;
|
||||
switchTab('general');
|
||||
hideAllDynamicFields();
|
||||
}
|
||||
|
||||
// 填充表单
|
||||
function populateForm(rule) {
|
||||
const form = document.getElementById('ruleForm');
|
||||
Object.keys(rule).forEach(key => {
|
||||
const field = form.querySelector(`[name="${key}"]`);
|
||||
if (field) {
|
||||
if (field.type === 'checkbox') {
|
||||
field.checked = rule[key];
|
||||
} else {
|
||||
field.value = rule[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 隐藏所有动态字段
|
||||
function hideAllDynamicFields() {
|
||||
const dynamicGroups = [
|
||||
'customProtocolGroup', 'localPortInputGroup', 'remotePortInputGroup',
|
||||
'programPathGroup', 'serviceGroup', 'serviceListGroup',
|
||||
'localIPInputGroup', 'remoteIPInputGroup', 'userInputGroup'
|
||||
];
|
||||
|
||||
dynamicGroups.forEach(groupId => {
|
||||
const group = document.getElementById(groupId);
|
||||
if (group) group.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
function switchTab(tabName) {
|
||||
// 移除所有标签的活动状态
|
||||
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// 激活选中的标签
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
}
|
||||
|
||||
// 协议设置切换
|
||||
function togglePortSettings() {
|
||||
const protocol = document.querySelector('[name="protocol"]').value;
|
||||
const customGroup = document.getElementById('customProtocolGroup');
|
||||
const localPortGroup = document.getElementById('localPortGroup');
|
||||
const remotePortGroup = document.getElementById('remotePortGroup');
|
||||
|
||||
if (protocol === 'custom') {
|
||||
customGroup.style.display = 'block';
|
||||
} else {
|
||||
customGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
if (['tcp', 'udp'].includes(protocol)) {
|
||||
localPortGroup.style.display = 'block';
|
||||
remotePortGroup.style.display = 'block';
|
||||
} else if (protocol === 'any') {
|
||||
localPortGroup.style.display = 'block';
|
||||
remotePortGroup.style.display = 'block';
|
||||
} else {
|
||||
localPortGroup.style.display = 'none';
|
||||
remotePortGroup.style.display = 'none';
|
||||
document.getElementById('localPortInputGroup').style.display = 'none';
|
||||
document.getElementById('remotePortInputGroup').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 本地端口输入切换
|
||||
function toggleLocalPortInput() {
|
||||
const portType = document.querySelector('[name="localPortType"]').value;
|
||||
const inputGroup = document.getElementById('localPortInputGroup');
|
||||
inputGroup.style.display = portType === 'all' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// 远程端口输入切换
|
||||
function toggleRemotePortInput() {
|
||||
const portType = document.querySelector('[name="remotePortType"]').value;
|
||||
const inputGroup = document.getElementById('remotePortInputGroup');
|
||||
inputGroup.style.display = portType === 'all' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// 程序设置切换
|
||||
function toggleProgramSettings() {
|
||||
const programType = document.querySelector('[name="programType"]').value;
|
||||
const programPathGroup = document.getElementById('programPathGroup');
|
||||
const serviceGroup = document.getElementById('serviceGroup');
|
||||
|
||||
programPathGroup.style.display = programType === 'specific' ? 'block' : 'none';
|
||||
serviceGroup.style.display = programType === 'service' ? 'block' : 'none';
|
||||
|
||||
if (programType !== 'service') {
|
||||
document.getElementById('serviceListGroup').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 服务选项切换
|
||||
function toggleServiceOptions() {
|
||||
const serviceType = document.querySelector('[name="serviceType"]').value;
|
||||
const serviceListGroup = document.getElementById('serviceListGroup');
|
||||
serviceListGroup.style.display = serviceType === 'specific' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 本地IP输入切换
|
||||
function toggleLocalIPInput() {
|
||||
const ipType = document.querySelector('[name="localIPType"]').value;
|
||||
const inputGroup = document.getElementById('localIPInputGroup');
|
||||
inputGroup.style.display = ipType === 'any' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// 远程IP输入切换
|
||||
function toggleRemoteIPInput() {
|
||||
const ipType = document.querySelector('[name="remoteIPType"]').value;
|
||||
const inputGroup = document.getElementById('remoteIPInputGroup');
|
||||
inputGroup.style.display = ipType === 'any' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// 用户输入切换
|
||||
function toggleUserInput() {
|
||||
const userType = document.querySelector('[name="userType"]').value;
|
||||
const inputGroup = document.getElementById('userInputGroup');
|
||||
inputGroup.style.display = userType === 'any' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// 浏览程序路径
|
||||
function browseProgramPath() {
|
||||
// 模拟文件浏览器
|
||||
const path = prompt('请输入程序路径:', 'C:\\Program Files\\');
|
||||
if (path) {
|
||||
document.querySelector('[name="programPath"]').value = path;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑规则
|
||||
function editRule(ruleId) {
|
||||
openModal('edit', ruleId);
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
function deleteRule(ruleId) {
|
||||
if (confirm('确定要删除这条规则吗?')) {
|
||||
firewallRules = firewallRules.filter(rule => rule.id !== ruleId);
|
||||
renderRulesTable();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存规则
|
||||
function saveRule() {
|
||||
const form = document.getElementById('ruleForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const ruleData = {
|
||||
name: formData.get('name'),
|
||||
direction: formData.get('direction'),
|
||||
action: formData.get('action'),
|
||||
protocol: formData.get('protocol'),
|
||||
localPort: formData.get('localPort') || 'Any',
|
||||
remotePort: formData.get('remotePort') || 'Any',
|
||||
program: formData.get('programPath') || 'Any',
|
||||
service: formData.get('service') || 'Any',
|
||||
enabled: formData.has('enabled'),
|
||||
profile: formData.get('profile'),
|
||||
localIP: formData.get('localIP') || 'Any',
|
||||
remoteIP: formData.get('remoteIP') || 'Any',
|
||||
users: formData.get('users') || 'Any',
|
||||
description: formData.get('description') || ''
|
||||
};
|
||||
|
||||
if (!ruleData.name) {
|
||||
alert('请输入规则名称');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode && currentRule) {
|
||||
// 更新现有规则
|
||||
Object.assign(currentRule, ruleData);
|
||||
} else {
|
||||
// 添加新规则
|
||||
ruleData.id = Date.now();
|
||||
firewallRules.unshift(ruleData);
|
||||
}
|
||||
|
||||
renderRulesTable();
|
||||
closeModal();
|
||||
|
||||
// 显示成功消息
|
||||
showNotification(isEditMode ? '规则已更新' : '规则已创建');
|
||||
}
|
||||
|
||||
// 刷新规则
|
||||
function refreshRules() {
|
||||
renderRulesTable();
|
||||
showNotification('规则已刷新');
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message) {
|
||||
// 创建一个简单的通知
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 15px 25px;
|
||||
border-radius: 5px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 添加CSS动画
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 点击模态框外部关闭
|
||||
document.getElementById('ruleModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,951 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
preset="card"
|
||||
:style="{ width: '1000px', height: '90vh' }"
|
||||
:title="isEdit ? '编辑防火墙规则' : '新建防火墙规则'"
|
||||
:closable="true"
|
||||
:auto-focus="false"
|
||||
:mask-closable="false"
|
||||
class="firewall-rule-modal"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="rule-form-container">
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-placement="top"
|
||||
require-mark-placement="right-hanging"
|
||||
class="firewall-form"
|
||||
>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs v-model:value="activeTab" type="line" size="medium" animated class="rule-tabs">
|
||||
<!-- 基本信息 -->
|
||||
<n-tab-pane name="basic" tab="基本信息">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="InformationCircleOutline" />
|
||||
规则标识
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="name" label="规则名称" required>
|
||||
<n-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="输入唯一的规则名称"
|
||||
maxlength="255"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="description" label="规则描述">
|
||||
<n-input
|
||||
v-model:value="formData.description"
|
||||
type="textarea"
|
||||
placeholder="描述此规则的用途和目的"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="grouping" label="规则分组">
|
||||
<n-input
|
||||
v-model:value="formData.grouping"
|
||||
placeholder="可选:输入分组名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="SettingsOutline" />
|
||||
规则行为
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<n-form-item path="enabled" label="规则状态">
|
||||
<n-radio-group v-model:value="formData.enabled">
|
||||
<n-radio :value="true">启用</n-radio>
|
||||
<n-radio :value="false">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="direction" label="规则方向" required>
|
||||
<n-radio-group v-model:value="formData.direction">
|
||||
<n-radio :value="1">入站 (Inbound)</n-radio>
|
||||
<n-radio :value="2">出站 (Outbound)</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<n-form-item path="action" label="规则动作" required>
|
||||
<n-radio-group v-model:value="formData.action">
|
||||
<n-radio :value="1">允许 (Allow)</n-radio>
|
||||
<n-radio :value="0">阻止 (Block)</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 应用程序和服务 -->
|
||||
<n-tab-pane name="application" tab="应用程序">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="AppsOutline" />
|
||||
应用程序路径
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="applicationName" label="应用程序">
|
||||
<n-input
|
||||
v-model:value="formData.applicationName"
|
||||
placeholder="例: C:\Program Files\Example\app.exe (留空表示所有程序)"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-button text @click="browseApplication">
|
||||
<n-icon :component="FolderOpenOutline" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="serviceName" label="系统服务">
|
||||
<n-select
|
||||
v-model:value="formData.serviceName"
|
||||
:options="serviceOptions"
|
||||
placeholder="选择系统服务 (可选)"
|
||||
filterable
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 协议和端口 -->
|
||||
<n-tab-pane name="protocol" tab="协议端口">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="LayersOutline" />
|
||||
协议设置
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="protocol" label="协议类型" required>
|
||||
<n-select
|
||||
v-model:value="formData.protocol"
|
||||
:options="protocolOptions"
|
||||
placeholder="选择协议类型"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div class="form-row">
|
||||
<n-form-item path="localPorts" label="本地端口">
|
||||
<n-input
|
||||
v-model:value="formData.localPorts"
|
||||
placeholder="例: 80, 443, 8080-8090 (留空表示所有端口)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="remotePorts" label="远程端口">
|
||||
<n-input
|
||||
v-model:value="formData.remotePorts"
|
||||
placeholder="例: 80, 443, 8080-8090 (留空表示所有端口)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<n-form-item
|
||||
v-if="formData.protocol === 1 || formData.protocol === 58"
|
||||
path="icmpTypesAndCodes"
|
||||
label="ICMP 类型和代码"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="formData.icmpTypesAndCodes"
|
||||
placeholder="例: 8:0 (留空表示所有ICMP类型)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 地址设置 -->
|
||||
<n-tab-pane name="address" tab="地址设置">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="GlobeOutline" />
|
||||
IP 地址范围
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="localAddresses" label="本地地址">
|
||||
<n-input
|
||||
v-model:value="formData.localAddresses"
|
||||
placeholder="例: 192.168.1.0/24, 10.0.0.1 (* 表示任何地址)"
|
||||
/>
|
||||
<template #feedback> 支持单个IP、CIDR格式或逗号分隔的多个地址</template>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="remoteAddresses" label="远程地址">
|
||||
<n-input
|
||||
v-model:value="formData.remoteAddresses"
|
||||
placeholder="例: 192.168.1.0/24, 10.0.0.1 (* 表示任何地址)"
|
||||
/>
|
||||
<template #feedback> 支持单个IP、CIDR格式或逗号分隔的多个地址</template>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 配置文件和接口 -->
|
||||
<n-tab-pane name="profile" tab="配置文件">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="ShieldCheckmarkOutline" />
|
||||
网络配置文件
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="profiles" label="应用于配置文件" required>
|
||||
<n-checkbox-group v-model:value="formData.profiles">
|
||||
<n-space vertical>
|
||||
<n-checkbox :value="1">
|
||||
<div class="profile-option">
|
||||
<div class="profile-name">域网络 (Domain)</div>
|
||||
<div class="profile-desc">计算机连接到域时</div>
|
||||
</div>
|
||||
</n-checkbox>
|
||||
<n-checkbox :value="2">
|
||||
<div class="profile-option">
|
||||
<div class="profile-name">专用网络 (Private)</div>
|
||||
<div class="profile-desc">家庭或工作网络</div>
|
||||
</div>
|
||||
</n-checkbox>
|
||||
<n-checkbox :value="4">
|
||||
<div class="profile-option">
|
||||
<div class="profile-name">公用网络 (Public)</div>
|
||||
<div class="profile-desc">公共场所的网络</div>
|
||||
</div>
|
||||
</n-checkbox>
|
||||
</n-space>
|
||||
</n-checkbox-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="interfaceTypes" label="接口类型">
|
||||
<n-input
|
||||
v-model:value="formData.interfaceTypes"
|
||||
placeholder="例: Wireless, Lan (留空或'All'表示所有接口)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="interfaces" label="特定接口">
|
||||
<n-dynamic-tags
|
||||
v-model:value="formData.interfaces"
|
||||
placeholder="添加网络接口名称"
|
||||
/>
|
||||
<template #feedback> 可以指定特定的网络接口,留空表示所有接口</template>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 高级选项 -->
|
||||
<n-tab-pane name="advanced" tab="高级选项">
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="CogOutline" />
|
||||
边缘遍历设置
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="edgeTraversal" label="边缘遍历">
|
||||
<n-checkbox v-model:checked="formData.edgeTraversal">
|
||||
允许边缘遍历 (适用于需要穿越NAT的应用)
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="edgeTraversalAllowed" label="边缘遍历策略">
|
||||
<n-checkbox v-model:checked="formData.edgeTraversalAllowed">
|
||||
允许延迟边缘遍历
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<n-icon :component="LockClosedOutline" />
|
||||
授权设置
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<n-form-item path="remoteMachineAuthorizationList" label="远程计算机授权">
|
||||
<n-input
|
||||
v-model:value="formData.remoteMachineAuthorizationList"
|
||||
placeholder="输入安全描述符 (高级用户)"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="remoteUserAuthorizationList" label="远程用户授权">
|
||||
<n-input
|
||||
v-model:value="formData.remoteUserAuthorizationList"
|
||||
placeholder="输入安全描述符 (高级用户)"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="embeddedContext" label="嵌入式上下文">
|
||||
<n-input
|
||||
v-model:value="formData.embeddedContext"
|
||||
placeholder="可选的嵌入式上下文信息"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-space>
|
||||
<n-button @click="handleClose">取消</n-button>
|
||||
<n-button @click="handleValidate">验证规则</n-button>
|
||||
<n-button type="primary" @click="handleSave" :loading="saving">
|
||||
<template #icon>
|
||||
<n-icon :component="CheckmarkOutline" />
|
||||
</template>
|
||||
{{ isEdit ? '更新规则' : '创建规则' }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import {
|
||||
NModal,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NRadioGroup,
|
||||
NRadio,
|
||||
NCheckbox,
|
||||
NCheckboxGroup,
|
||||
NDynamicTags,
|
||||
NButton,
|
||||
NIcon,
|
||||
NSpace,
|
||||
FormInst,
|
||||
FormRules,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
CheckmarkOutline,
|
||||
InformationCircleOutline,
|
||||
SettingsOutline,
|
||||
AppsOutline,
|
||||
LayersOutline,
|
||||
GlobeOutline,
|
||||
ShieldCheckmarkOutline,
|
||||
CogOutline,
|
||||
LockClosedOutline,
|
||||
FolderOpenOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
// 枚举定义 - 对应后端模型
|
||||
enum RuleDirection {
|
||||
Inbound = 1,
|
||||
Outbound = 2,
|
||||
}
|
||||
|
||||
enum RuleAction {
|
||||
Block = 0,
|
||||
Allow = 1,
|
||||
}
|
||||
|
||||
enum ProtocolType {
|
||||
Any = 256,
|
||||
ICMPV4 = 1,
|
||||
IGMP = 2,
|
||||
TCP = 6,
|
||||
UDP = 17,
|
||||
IPv6 = 41,
|
||||
IPv6Route = 43,
|
||||
IPv6Frag = 44,
|
||||
GRE = 47,
|
||||
ICMPv6 = 58,
|
||||
IPv6NoNxt = 59,
|
||||
IPv6Opts = 60,
|
||||
VRRP = 112,
|
||||
PGM = 113,
|
||||
L2TP = 115,
|
||||
}
|
||||
|
||||
enum FirewallProfile {
|
||||
Domain = 1,
|
||||
Private = 2,
|
||||
Public = 4,
|
||||
}
|
||||
|
||||
// 防火墙规则接口 - 对应后端 FirewallRule
|
||||
interface FirewallRule {
|
||||
name: string
|
||||
description: string
|
||||
applicationName: string
|
||||
serviceName: string
|
||||
protocol: ProtocolType
|
||||
localPorts: string
|
||||
remotePorts: string
|
||||
localAddresses: string
|
||||
remoteAddresses: string
|
||||
icmpTypesAndCodes: string
|
||||
direction: RuleDirection
|
||||
enabled: boolean
|
||||
profiles: FirewallProfile[]
|
||||
edgeTraversal: boolean
|
||||
action: RuleAction
|
||||
grouping: string
|
||||
interfaceTypes: string
|
||||
interfaces: string[]
|
||||
|
||||
// 高级属性
|
||||
edgeTraversalAllowed: boolean
|
||||
looseSourceMapping: boolean
|
||||
localOnlyMapping: boolean
|
||||
remoteMachineAuthorizationList: string
|
||||
remoteUserAuthorizationList: string
|
||||
embeddedContext: string
|
||||
flags: number
|
||||
secureFlags: boolean
|
||||
}
|
||||
|
||||
// 创建规则请求接口 - 对应后端 CreateRuleRequest
|
||||
interface CreateRuleRequest extends Omit<FirewallRule, 'profiles'> {
|
||||
profiles: number // 使用位标志表示多个配置文件
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
editRule?: FirewallRule | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
save: [rule: CreateRuleRequest]
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const message = useMessage()
|
||||
const formRef = ref<FormInst>()
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!props.editRule)
|
||||
const activeTab = ref('basic')
|
||||
const saving = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const defaultFormData: FirewallRule = {
|
||||
name: '',
|
||||
description: '',
|
||||
applicationName: '',
|
||||
serviceName: '',
|
||||
protocol: ProtocolType.Any,
|
||||
localPorts: '',
|
||||
remotePorts: '',
|
||||
localAddresses: '*',
|
||||
remoteAddresses: '*',
|
||||
icmpTypesAndCodes: '',
|
||||
direction: RuleDirection.Inbound,
|
||||
enabled: true,
|
||||
profiles: [FirewallProfile.Domain, FirewallProfile.Private],
|
||||
edgeTraversal: false,
|
||||
action: RuleAction.Allow,
|
||||
grouping: '',
|
||||
interfaceTypes: 'All',
|
||||
interfaces: [],
|
||||
|
||||
// 高级属性
|
||||
edgeTraversalAllowed: false,
|
||||
looseSourceMapping: false,
|
||||
localOnlyMapping: false,
|
||||
remoteMachineAuthorizationList: '',
|
||||
remoteUserAuthorizationList: '',
|
||||
embeddedContext: '',
|
||||
flags: 0,
|
||||
secureFlags: false,
|
||||
}
|
||||
|
||||
const formData = ref<FirewallRule>({ ...defaultFormData })
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入规则名称', trigger: 'blur' },
|
||||
{ min: 1, max: 255, message: '规则名称长度应在1-255字符之间', trigger: 'blur' },
|
||||
],
|
||||
direction: [{ required: true, message: '请选择规则方向', trigger: 'change', type: 'number' }],
|
||||
action: [{ required: true, message: '请选择规则动作', trigger: 'change', type: 'number' }],
|
||||
protocol: [{ required: true, message: '请选择协议类型', trigger: 'change', type: 'number' }],
|
||||
profiles: [
|
||||
{
|
||||
required: true,
|
||||
message: '请至少选择一个配置文件',
|
||||
trigger: 'change',
|
||||
validator: (rule, value: FirewallProfile[]) => {
|
||||
return value && value.length > 0
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 选项数据
|
||||
const protocolOptions = [
|
||||
{ label: '任意协议', value: ProtocolType.Any },
|
||||
{ label: 'TCP', value: ProtocolType.TCP },
|
||||
{ label: 'UDP', value: ProtocolType.UDP },
|
||||
{ label: 'ICMPv4', value: ProtocolType.ICMPV4 },
|
||||
{ label: 'ICMPv6', value: ProtocolType.ICMPv6 },
|
||||
{ label: 'IGMP', value: ProtocolType.IGMP },
|
||||
{ label: 'IPv6', value: ProtocolType.IPv6 },
|
||||
{ label: 'GRE', value: ProtocolType.GRE },
|
||||
{ label: 'VRRP', value: ProtocolType.VRRP },
|
||||
{ label: 'PGM', value: ProtocolType.PGM },
|
||||
{ label: 'L2TP', value: ProtocolType.L2TP },
|
||||
]
|
||||
|
||||
const serviceOptions = [
|
||||
{ label: 'Windows Update (wuauserv)', value: 'wuauserv' },
|
||||
{ label: '远程桌面服务 (TermService)', value: 'TermService' },
|
||||
{ label: 'Windows Defender (WinDefend)', value: 'WinDefend' },
|
||||
{ label: '打印后台处理程序 (Spooler)', value: 'Spooler' },
|
||||
{ label: 'Windows 防火墙 (MpsSvc)', value: 'MpsSvc' },
|
||||
{ label: 'DNS 客户端 (Dnscache)', value: 'Dnscache' },
|
||||
{ label: 'DHCP 客户端 (Dhcp)', value: 'Dhcp' },
|
||||
{ label: 'Workstation (LanmanWorkstation)', value: 'LanmanWorkstation' },
|
||||
{ label: 'Server (LanmanServer)', value: 'LanmanServer' },
|
||||
]
|
||||
|
||||
// 监听编辑规则变化
|
||||
watch(
|
||||
() => props.editRule,
|
||||
(newRule) => {
|
||||
if (newRule) {
|
||||
formData.value = { ...newRule }
|
||||
} else {
|
||||
formData.value = { ...defaultFormData }
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 方法
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
setTimeout(() => {
|
||||
formData.value = { ...defaultFormData }
|
||||
activeTab.value = 'basic'
|
||||
formRef.value?.restoreValidation()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const handleValidate = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
message.success('规则验证通过')
|
||||
} catch (errors) {
|
||||
message.error('请检查表单中的错误')
|
||||
console.log(errors)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
saving.value = true
|
||||
|
||||
// 转换数据格式以匹配后端要求
|
||||
const saveData: CreateRuleRequest = {
|
||||
...formData.value,
|
||||
// 将配置文件数组转换为位标志
|
||||
profiles: formData.value.profiles.reduce((acc, profile) => acc | profile, 0),
|
||||
}
|
||||
|
||||
// 模拟保存延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
emit('save', saveData)
|
||||
message.success(isEdit.value ? '规则更新成功' : '规则创建成功')
|
||||
handleClose()
|
||||
} catch (errors) {
|
||||
message.error('请检查表单中的错误')
|
||||
console.log(errors)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const browseApplication = () => {
|
||||
// 这里可以集成文件选择器或调用 Electron API
|
||||
message.info('请使用文件浏览器选择应用程序')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础模态框样式 */
|
||||
:deep(.firewall-rule-modal .n-card) {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--border-primary);
|
||||
box-shadow: var(--shadow-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__header) {
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
padding: 20px 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__content) {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__content),
|
||||
.rule-form-container,
|
||||
.firewall-form,
|
||||
.rule-tabs,
|
||||
:deep(.rule-tabs .n-tabs-pane-wrapper),
|
||||
:deep(.rule-tabs .n-tabs-pane) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.firewall-rule-modal .n-card__footer) {
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
padding: 16px 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.rule-form-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.firewall-form {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
.rule-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-nav) {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
padding: 0 24px;
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-pane-wrapper) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-pane) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 标签页内容 */
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
scroll-behavior: smooth;
|
||||
min-height: 0;
|
||||
/* 新增:计算可用高度 */
|
||||
max-height: calc(90vh - 220px);
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb {
|
||||
background: var(--border-secondary);
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 表单区块样式 */
|
||||
.form-section {
|
||||
margin-bottom: 28px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-quaternary);
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title .n-icon {
|
||||
color: var(--accent-primary);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 表单网格 */
|
||||
.form-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 配置文件选项样式 */
|
||||
.profile-option {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.profile-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 模态框底部 */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 标签页切换动画 */
|
||||
:deep(.n-tabs-tab) {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
:deep(.n-tabs-tab:hover) {
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
:deep(.n-tabs-tab--active) {
|
||||
color: var(--accent-primary) !important;
|
||||
background: var(--bg-card);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Naive UI 组件样式覆盖 */
|
||||
:deep(.n-form-item-label) {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.n-form-item-feedback) {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
--n-color: var(--bg-card);
|
||||
--n-text-color: var(--text-secondary);
|
||||
--n-border-radius: 6px;
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
--n-color: var(--bg-card);
|
||||
--n-text-color: var(--text-secondary);
|
||||
--n-border-radius: 6px;
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-checkbox) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-checked: 1px solid var(--accent-primary);
|
||||
--n-color-checked: var(--accent-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-radio) {
|
||||
--n-dot-color-active: var(--accent-primary);
|
||||
--n-color-active: var(--accent-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-checkbox-group) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-radio-group) {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 标签页内容切换动画 */
|
||||
.tab-content {
|
||||
animation: fadeInSlide 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
:deep(.firewall-rule-modal .n-card) {
|
||||
width: 90vw !important;
|
||||
height: 90vh !important;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.n-radio-group) {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.rule-tabs .n-tabs-nav) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.tab-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,19 +8,40 @@
|
||||
<p class="view-subtitle">配置和管理网络防护规则</p>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<n-button type="primary" size="medium">
|
||||
<!-- 防火墙总开关 -->
|
||||
<div class="firewall-status">
|
||||
<span class="status-label">防火墙状态</span>
|
||||
<n-switch v-model:value="firewallEnabled" size="medium" />
|
||||
</div>
|
||||
<n-button type="primary" size="medium" @click="openRuleForm()">
|
||||
<template #icon>
|
||||
<n-icon :component="AddOutline" />
|
||||
</template>
|
||||
添加规则
|
||||
新建规则
|
||||
</n-button>
|
||||
<n-button size="medium" ghost>导入规则</n-button>
|
||||
<n-button size="medium" ghost>导出规则</n-button>
|
||||
<n-dropdown :options="batchOptions" placement="bottom-end">
|
||||
<n-button size="medium" ghost>
|
||||
批量操作
|
||||
<template #icon>
|
||||
<n-icon :component="ChevronDownOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规则统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper primary">
|
||||
<n-icon :component="ListOutline" size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.total }}</div>
|
||||
<div class="stat-label">总规则数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper success">
|
||||
<n-icon :component="CheckmarkCircleOutline" size="24" />
|
||||
@@ -47,17 +68,7 @@
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.blocked }}</div>
|
||||
<div class="stat-label">已拦截</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper info">
|
||||
<n-icon :component="ShieldCheckmarkOutline" size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.total }}</div>
|
||||
<div class="stat-label">总规则数</div>
|
||||
<div class="stat-label">阻止连接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,177 +80,399 @@
|
||||
<div class="panel-controls">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索规则..."
|
||||
placeholder="搜索规则名称..."
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon :component="SearchOutline" />
|
||||
</template>
|
||||
</n-input>
|
||||
<n-select
|
||||
v-model:value="filterType"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
:options="filterOptions"
|
||||
/>
|
||||
<n-dropdown :options="filterDropdownOptions" placement="bottom-end">
|
||||
<n-button size="small" ghost>
|
||||
<template #icon>
|
||||
<n-icon :component="FilterOutline" />
|
||||
</template>
|
||||
筛选
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rules-list">
|
||||
<div v-for="rule in mockRules" :key="rule.id" class="rule-item">
|
||||
<div class="rule-status">
|
||||
<n-switch v-model:value="rule.enabled" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="rule-main">
|
||||
<div class="rule-header">
|
||||
<h4 class="rule-name">{{ rule.name }}</h4>
|
||||
<n-tag :type="rule.action === 'allow' ? 'success' : 'error'" size="small">
|
||||
{{ rule.action === 'allow' ? '允许' : '拒绝' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
<div class="rule-details">
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="LocationOutline" size="14" />
|
||||
{{ rule.source }}
|
||||
</span>
|
||||
<span class="rule-arrow">→</span>
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="LocationOutline" size="14" />
|
||||
{{ rule.destination }}
|
||||
</span>
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="LayersOutline" size="14" />
|
||||
{{ rule.protocol }}
|
||||
</span>
|
||||
<span class="rule-detail">
|
||||
<n-icon :component="ServerOutline" size="14" />
|
||||
端口: {{ rule.port }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-actions">
|
||||
<n-button size="tiny" circle quaternary>
|
||||
<template #icon>
|
||||
<n-icon :component="CreateOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="tiny" circle quaternary>
|
||||
<template #icon>
|
||||
<n-icon :component="CopyOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="tiny" circle quaternary type="error">
|
||||
<template #icon>
|
||||
<n-icon :component="TrashOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="filteredRules"
|
||||
:pagination="false"
|
||||
:scroll-x="1200"
|
||||
:virtual-scroll="true"
|
||||
:max-height="600"
|
||||
size="small"
|
||||
striped
|
||||
:row-key="(row) => row.id"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防火墙规则表单 -->
|
||||
<FirewallRuleForm
|
||||
v-model="showRuleForm"
|
||||
:edit-rule="currentEditRule"
|
||||
@save="handleSaveRule"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { NButton, NIcon, NInput, NSelect, NSwitch, NTag } from 'naive-ui'
|
||||
import { ref, computed, h } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NIcon,
|
||||
NInput,
|
||||
NSwitch,
|
||||
NTag,
|
||||
NDataTable,
|
||||
NDropdown,
|
||||
useMessage,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
|
||||
import {
|
||||
AddOutline,
|
||||
SearchOutline,
|
||||
CheckmarkCircleOutline,
|
||||
PauseCircleOutline,
|
||||
CloseCircleOutline,
|
||||
ShieldCheckmarkOutline,
|
||||
LocationOutline,
|
||||
LayersOutline,
|
||||
ServerOutline,
|
||||
ListOutline,
|
||||
ChevronDownOutline,
|
||||
FilterOutline,
|
||||
CreateOutline,
|
||||
CopyOutline,
|
||||
TrashOutline,
|
||||
ArrowUpOutline,
|
||||
ArrowDownOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 界面状态
|
||||
const showRuleForm = ref(false)
|
||||
const currentEditRule = ref(null)
|
||||
const firewallEnabled = ref(true)
|
||||
const checkedRowKeys = ref<string[]>([])
|
||||
|
||||
// 搜索和过滤
|
||||
const searchQuery = ref('')
|
||||
const filterType = ref('all')
|
||||
|
||||
const filterOptions = [
|
||||
{ label: '全部规则', value: 'all' },
|
||||
{ label: '已启用', value: 'active' },
|
||||
{ label: '已禁用', value: 'inactive' },
|
||||
{ label: '入站规则', value: 'inbound' },
|
||||
{ label: '出站规则', value: 'outbound' },
|
||||
// 批量操作选项
|
||||
const batchOptions = [
|
||||
{
|
||||
label: '启用选中',
|
||||
key: 'enable',
|
||||
icon: () => h(NIcon, { component: CheckmarkCircleOutline })
|
||||
},
|
||||
{
|
||||
label: '禁用选中',
|
||||
key: 'disable',
|
||||
icon: () => h(NIcon, { component: PauseCircleOutline })
|
||||
},
|
||||
{
|
||||
label: '删除选中',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, { component: TrashOutline })
|
||||
}
|
||||
]
|
||||
|
||||
// 筛选下拉选项
|
||||
const filterDropdownOptions = [
|
||||
{
|
||||
label: '方向',
|
||||
key: 'direction-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '入站规则', key: 'inbound' },
|
||||
{ label: '出站规则', key: 'outbound' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '操作',
|
||||
key: 'action-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '允许', key: 'allow' },
|
||||
{ label: '阻止', key: 'block' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
key: 'status-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '已启用', key: 'enabled' },
|
||||
{ label: '已禁用', key: 'disabled' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
active: 128,
|
||||
inactive: 24,
|
||||
blocked: 3456,
|
||||
total: 152,
|
||||
total: 127,
|
||||
active: 89,
|
||||
inactive: 38,
|
||||
blocked: 1247,
|
||||
})
|
||||
|
||||
// 模拟规则数据
|
||||
const mockRules = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '阻止恶意IP访问',
|
||||
id: '1',
|
||||
name: 'Windows 远程桌面',
|
||||
description: '允许远程桌面连接',
|
||||
enabled: true,
|
||||
action: 'deny',
|
||||
source: '10.0.0.0/8',
|
||||
destination: '任意',
|
||||
protocol: 'TCP/UDP',
|
||||
port: '全部',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
program: '系统服务',
|
||||
protocol: 'TCP',
|
||||
port: '3389',
|
||||
profiles: ['域', '专用'],
|
||||
priority: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '允许HTTPS流量',
|
||||
id: '2',
|
||||
name: 'Microsoft Edge',
|
||||
description: '允许浏览器访问网络',
|
||||
enabled: true,
|
||||
direction: 'outbound',
|
||||
action: 'allow',
|
||||
source: '任意',
|
||||
destination: '任意',
|
||||
protocol: 'TCP',
|
||||
port: '443',
|
||||
program: 'C:\\Program Files\\Microsoft\\Edge\\msedge.exe',
|
||||
protocol: '任意',
|
||||
port: '任意',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 200,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '允许DNS查询',
|
||||
id: '3',
|
||||
name: '阻止 Telnet',
|
||||
description: '安全策略:阻止 Telnet 连接',
|
||||
enabled: true,
|
||||
action: 'allow',
|
||||
source: '192.168.1.0/24',
|
||||
destination: '8.8.8.8',
|
||||
protocol: 'UDP',
|
||||
port: '53',
|
||||
direction: 'inbound',
|
||||
action: 'block',
|
||||
program: '任意',
|
||||
protocol: 'TCP',
|
||||
port: '23',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 300,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '阻止Telnet访问',
|
||||
id: '4',
|
||||
name: '文件和打印机共享',
|
||||
description: '允许局域网内文件共享',
|
||||
enabled: false,
|
||||
action: 'deny',
|
||||
source: '任意',
|
||||
destination: '任意',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
program: '系统服务',
|
||||
protocol: 'TCP',
|
||||
port: '23',
|
||||
port: '445, 139',
|
||||
profiles: ['专用'],
|
||||
priority: 400,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '允许SSH管理',
|
||||
id: '5',
|
||||
name: 'HTTP 服务器',
|
||||
description: '允许 Web 服务器接收请求',
|
||||
enabled: true,
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
source: '192.168.1.100',
|
||||
destination: '192.168.1.1',
|
||||
program: '任意',
|
||||
protocol: 'TCP',
|
||||
port: '22',
|
||||
port: '80, 443',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 500,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns: DataTableColumns = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 40
|
||||
},
|
||||
{
|
||||
title: '规则名称',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'rule-name-cell' }, [
|
||||
h('div', { class: 'rule-name' }, row.name),
|
||||
h('div', { class: 'rule-description' }, row.description)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enabled',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h(NTag, {
|
||||
type: row.enabled ? 'success' : 'default',
|
||||
size: 'small'
|
||||
}, {
|
||||
default: () => row.enabled ? '启用' : '禁用'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
key: 'direction',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'direction-cell' }, [
|
||||
h(NIcon, {
|
||||
component: row.direction === 'inbound' ? ArrowDownOutline : ArrowUpOutline,
|
||||
size: 16,
|
||||
style: { marginRight: '4px' }
|
||||
}),
|
||||
row.direction === 'inbound' ? '入站' : '出站'
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h(NTag, {
|
||||
type: row.action === 'allow' ? 'success' : 'error',
|
||||
size: 'small'
|
||||
}, {
|
||||
default: () => row.action === 'allow' ? '允许' : '阻止'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '程序/服务',
|
||||
key: 'program',
|
||||
width: 250,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '协议',
|
||||
key: 'protocol',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '配置文件',
|
||||
key: 'profiles',
|
||||
width: 150,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'profiles-cell' },
|
||||
row.profiles.map((profile: string) =>
|
||||
h(NTag, {
|
||||
key: profile,
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
style: { marginRight: '4px', marginBottom: '2px' }
|
||||
}, {
|
||||
default: () => profile
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'action-buttons' }, [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
onClick: () => editRule(row)
|
||||
}, {
|
||||
default: () => '编辑'
|
||||
}),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
type: 'error',
|
||||
onClick: () => deleteRule(row.id)
|
||||
}, {
|
||||
default: () => '删除'
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredRules = computed(() => {
|
||||
let rules = mockRules.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
rules = rules.filter(rule =>
|
||||
rule.name.toLowerCase().includes(query) ||
|
||||
rule.description.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return rules
|
||||
})
|
||||
|
||||
// 方法
|
||||
const openRuleForm = (rule: any = null) => {
|
||||
currentEditRule.value = rule
|
||||
showRuleForm.value = true
|
||||
}
|
||||
|
||||
const editRule = (rule: any) => {
|
||||
currentEditRule.value = { ...rule }
|
||||
showRuleForm.value = true
|
||||
}
|
||||
|
||||
const deleteRule = (id: string) => {
|
||||
const index = mockRules.value.findIndex(rule => rule.id === id)
|
||||
if (index !== -1) {
|
||||
mockRules.value.splice(index, 1)
|
||||
message.success('规则删除成功')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheck = (keys: string[]) => {
|
||||
checkedRowKeys.value = keys
|
||||
}
|
||||
|
||||
const handleSaveRule = (rule: any) => {
|
||||
if (rule.id) {
|
||||
// 编辑现有规则
|
||||
const index = mockRules.value.findIndex(r => r.id === rule.id)
|
||||
if (index !== -1) {
|
||||
mockRules.value[index] = rule
|
||||
message.success('规则更新成功')
|
||||
}
|
||||
} else {
|
||||
// 添加新规则
|
||||
rule.id = Date.now().toString()
|
||||
mockRules.value.unshift(rule)
|
||||
message.success('规则创建成功')
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
currentEditRule.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -261,6 +494,7 @@ const mockRules = ref([
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
@@ -282,9 +516,26 @@ const mockRules = ref([
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.firewall-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
@@ -319,6 +570,11 @@ const mockRules = ref([
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.primary {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--accent-success);
|
||||
@@ -334,11 +590,6 @@ const mockRules = ref([
|
||||
color: var(--accent-error);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -381,120 +632,132 @@ const mockRules = ref([
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 规则列表 */
|
||||
.rules-list {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.rule-item:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.rule-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rule-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rule-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.rule-details {
|
||||
/* 表格单元格样式 */
|
||||
.rule-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.rule-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rule-arrow {
|
||||
color: var(--text-quaternary);
|
||||
.rule-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
.rule-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.direction-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.profiles-cell {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1200px) {
|
||||
.firewall-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.firewall-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
.toolbar-right {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule-details {
|
||||
flex-wrap: wrap;
|
||||
.firewall-status {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* Naive UI 样式覆盖 */
|
||||
/* Naive UI 数据表格样式覆盖 */
|
||||
:deep(.n-data-table) {
|
||||
--n-td-color: var(--bg-card);
|
||||
--n-td-color-striped: var(--bg-tertiary);
|
||||
--n-th-color: var(--bg-secondary);
|
||||
--n-border-color: var(--border-secondary);
|
||||
--n-th-text-color: var(--text-secondary);
|
||||
--n-td-text-color: var(--text-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-th) {
|
||||
font-weight: 600;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
:deep(.n-data-table-td) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-th) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
@@ -503,12 +766,9 @@ const mockRules = ref([
|
||||
--n-text-color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
:deep(.n-dropdown-menu) {
|
||||
--n-color: var(--bg-card);
|
||||
--n-text-color: var(--text-primary);
|
||||
--n-option-text-color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-switch) {
|
||||
@@ -517,5 +777,6 @@ const mockRules = ref([
|
||||
|
||||
:deep(.n-tag) {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user