Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
ccd3e7906c
16 files changed
+709
-2655
No files matched your search
@@ -7,7 +7,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms> <!-- 加上这句,才能正确启用 Forms 支持 -->
|
||||
<!-- <ApplicationManifest>app.manifest</ApplicationManifest>-->
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<!-- Windows 兼容性声明 -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{4f476546-9373-4e33-b969-1e0b7c6f6d46}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- DPI 设置 -->
|
||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAwareness>PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
@@ -1,5 +1,2 @@
|
||||
VITE_MODE='development'
|
||||
VITE_APP_API_URL='http://localhost:8268/api'
|
||||
|
||||
我想让FirewallRuleForm.vue的滚动区域,只在页面主容器内进行,比如“基础内容”很多,那么就在“基础内容”的
|
||||
子Tab中进行滚动,不能超出去,不然滚动时将会整体滚动,非常不优雅,而且你这次优化的样式我觉得还有提升空间,你可以参考MonitorView下的组件进行样式优化
|
||||
VITE_APP_API_URL='http://localhost:8268/api'
|
||||
@@ -1,1083 +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 {
|
||||
--bg-primary: #0a0f1c;
|
||||
--bg-secondary: #131a2a;
|
||||
--bg-card: rgba(20, 27, 45, 0.8);
|
||||
--text-primary: #e4e8f1;
|
||||
--text-secondary: #8b92a8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--glass: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 背景动画 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.1) 0%, transparent 50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--success) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--accent) 0%, var(--success) 100%);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 过滤和操作栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--glass);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 10px 40px 10px 15px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 规则表格 */
|
||||
.rules-table {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--bg-secondary);
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 15px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tr {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.direction-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* 模态框 */
|
||||
.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.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 20px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: slideUp 0.3s ease;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 服务选择器 */
|
||||
.service-selector {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: var(--glass);
|
||||
}
|
||||
|
||||
.service-item {
|
||||
padding: 8px;
|
||||
margin: 2px 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.service-item input {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
max-height: 95vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>防火墙管理中心</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-secondary" onclick="exportRules()">
|
||||
<span>📤</span> 导出规则
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="importRules()">
|
||||
<span>📥</span> 导入规则
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总规则数</div>
|
||||
<div class="stat-value" id="total-rules">42</div>
|
||||
<div class="stat-label">入站: 24 | 出站: 18</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">已启用规则</div>
|
||||
<div class="stat-value" style="color: var(--success);">35</div>
|
||||
<div class="stat-label">占比 83.3%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">已禁用规则</div>
|
||||
<div class="stat-value" style="color: var(--danger);">7</div>
|
||||
<div class="stat-label">占比 16.7%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">安全等级</div>
|
||||
<div class="stat-value" style="color: var(--success);">高</div>
|
||||
<div class="stat-label">系统防护良好</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 过滤和操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="filter-group">
|
||||
<button class="btn btn-secondary" 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-secondary" onclick="filterRules('enabled')">已启用</button>
|
||||
<button class="btn btn-secondary" onclick="filterRules('disabled')">已禁用</button>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="搜索规则名称、程序、端口..." onkeyup="searchRules(this.value)">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openAddModal()">
|
||||
<span>➕</span> 新建规则
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 规则表格 -->
|
||||
<div class="rules-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>状态</th>
|
||||
<th>名称</th>
|
||||
<th>方向</th>
|
||||
<th>程序</th>
|
||||
<th>协议</th>
|
||||
<th>端口</th>
|
||||
<th>远程地址</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rules-tbody">
|
||||
<!-- 动态生成的规则行 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑规则模态框 -->
|
||||
<div class="modal" id="rule-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">新建防火墙规则</h2>
|
||||
<button class="close-btn" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 标签页 -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('general')">常规</button>
|
||||
<button class="tab" onclick="switchTab('program')">程序和服务</button>
|
||||
<button class="tab" onclick="switchTab('protocol')">协议和端口</button>
|
||||
<button class="tab" onclick="switchTab('scope')">作用域</button>
|
||||
<button class="tab" onclick="switchTab('advanced')">高级</button>
|
||||
</div>
|
||||
|
||||
<!-- 常规标签页 -->
|
||||
<div class="tab-content active" id="general-tab">
|
||||
<div class="form-group">
|
||||
<label>规则名称 *</label>
|
||||
<input type="text" class="form-control" id="rule-name" placeholder="输入规则名称">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<textarea class="form-control" id="rule-description" rows="3" placeholder="输入规则描述"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>方向 *</label>
|
||||
<select class="form-control" id="rule-direction">
|
||||
<option value="inbound">入站</option>
|
||||
<option value="outbound">出站</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>操作 *</label>
|
||||
<select class="form-control" id="rule-action">
|
||||
<option value="allow">允许连接</option>
|
||||
<option value="block">阻止连接</option>
|
||||
<option value="allow-secure">允许安全连接</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="rule-enabled" checked>
|
||||
启用此规则
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 程序和服务标签页 -->
|
||||
<div class="tab-content" id="program-tab">
|
||||
<div class="form-group">
|
||||
<label>程序路径</label>
|
||||
<input type="text" class="form-control" id="program-path" placeholder="例如: C:\Program Files\App\app.exe">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>应用于以下服务</label>
|
||||
<div class="service-selector">
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="BITS"> 后台智能传输服务 (BITS)</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="Dnscache"> DNS 客户端 (Dnscache)</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="Dhcp"> DHCP 客户端 (Dhcp)</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="EventLog"> Windows 事件日志 (EventLog)</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="MpsSvc"> Windows 防火墙 (MpsSvc)</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="RpcSs"> 远程过程调用 (RPC)</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="Spooler"> 打印后台处理程序</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="W32Time"> Windows 时间</label>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label><input type="checkbox" value="WinRM"> Windows 远程管理</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 协议和端口标签页 -->
|
||||
<div class="tab-content" id="protocol-tab">
|
||||
<div class="form-group">
|
||||
<label>协议类型</label>
|
||||
<select class="form-control" id="protocol-type" onchange="togglePortFields()">
|
||||
<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="custom-protocol-group" style="display: none;">
|
||||
<label>协议号</label>
|
||||
<input type="number" class="form-control" id="protocol-number" min="0" max="255" placeholder="0-255">
|
||||
</div>
|
||||
<div class="form-row" id="port-fields">
|
||||
<div class="form-group">
|
||||
<label>本地端口</label>
|
||||
<input type="text" class="form-control" id="local-ports" placeholder="例如: 80, 443, 1000-2000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>远程端口</label>
|
||||
<input type="text" class="form-control" id="remote-ports" placeholder="例如: 80, 443, 1000-2000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作用域标签页 -->
|
||||
<div class="tab-content" id="scope-tab">
|
||||
<div class="form-group">
|
||||
<label>本地 IP 地址</label>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="radio" name="local-ip" value="any" checked>
|
||||
<label>任何 IP 地址</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="radio" name="local-ip" value="custom">
|
||||
<label>指定 IP 地址</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="local-ip-list" placeholder="例如: 192.168.1.0/24, 10.0.0.1" style="margin-top: 10px; display: none;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>远程 IP 地址</label>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="radio" name="remote-ip" value="any" checked>
|
||||
<label>任何 IP 地址</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="radio" name="remote-ip" value="custom">
|
||||
<label>指定 IP 地址</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="remote-ip-list" placeholder="例如: 192.168.1.0/24, 10.0.0.1" style="margin-top: 10px; display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级标签页 -->
|
||||
<div class="tab-content" id="advanced-tab">
|
||||
<div class="form-group">
|
||||
<label>配置文件</label>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="profile-domain" checked>
|
||||
<label>域</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="profile-private" checked>
|
||||
<label>专用</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="profile-public" checked>
|
||||
<label>公用</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>用户和组</label>
|
||||
<input type="text" class="form-control" id="users-groups" placeholder="例如: DOMAIN\User, Everyone">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>边缘遍历</label>
|
||||
<select class="form-control" id="edge-traversal">
|
||||
<option value="block">阻止边缘遍历</option>
|
||||
<option value="allow">允许边缘遍历</option>
|
||||
<option value="defer-to-user">延迟到用户</option>
|
||||
<option value="defer-to-app">延迟到应用程序</option>
|
||||
</select>
|
||||
</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>
|
||||
// 模拟规则数据
|
||||
let rules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "允许 Chrome 浏览器",
|
||||
enabled: true,
|
||||
direction: "outbound",
|
||||
program: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
protocol: "TCP",
|
||||
localPort: "任意",
|
||||
remotePort: "80, 443",
|
||||
remoteAddress: "任意",
|
||||
action: "allow",
|
||||
description: "允许 Chrome 浏览器访问网络"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "远程桌面 - 用户模式 (TCP-In)",
|
||||
enabled: true,
|
||||
direction: "inbound",
|
||||
program: "系统",
|
||||
protocol: "TCP",
|
||||
localPort: "3389",
|
||||
remotePort: "任意",
|
||||
remoteAddress: "任意",
|
||||
action: "allow",
|
||||
description: "允许远程桌面连接"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "阻止 SMB 445 端口",
|
||||
enabled: true,
|
||||
direction: "inbound",
|
||||
program: "任意",
|
||||
protocol: "TCP",
|
||||
localPort: "445",
|
||||
remotePort: "任意",
|
||||
remoteAddress: "任意",
|
||||
action: "block",
|
||||
description: "阻止 SMB 文件共享端口"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Windows 更新服务",
|
||||
enabled: true,
|
||||
direction: "outbound",
|
||||
program: "C:\\Windows\\System32\\svchost.exe",
|
||||
protocol: "TCP/UDP",
|
||||
localPort: "任意",
|
||||
remotePort: "80, 443",
|
||||
remoteAddress: "任意",
|
||||
action: "allow",
|
||||
description: "允许 Windows 更新服务"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "SSH 服务器",
|
||||
enabled: false,
|
||||
direction: "inbound",
|
||||
program: "C:\\Program Files\\OpenSSH\\sshd.exe",
|
||||
protocol: "TCP",
|
||||
localPort: "22",
|
||||
remotePort: "任意",
|
||||
remoteAddress: "192.168.1.0/24",
|
||||
action: "allow",
|
||||
description: "SSH 服务器访问规则"
|
||||
}
|
||||
];
|
||||
|
||||
let currentEditId = null;
|
||||
|
||||
// 渲染规则表格
|
||||
function renderRules() {
|
||||
const tbody = document.getElementById('rules-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
rules.forEach(rule => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.onclick = () => editRule(rule.id);
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<span class="status-badge ${rule.enabled ? 'status-enabled' : 'status-disabled'}">
|
||||
<span style="font-size: 0.8rem;">${rule.enabled ? '●' : '○'}</span>
|
||||
${rule.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${rule.name}</td>
|
||||
<td>
|
||||
<span class="direction-badge">
|
||||
${rule.direction === 'inbound' ? '⬅️ 入站' : '➡️ 出站'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${rule.program.length > 30 ? '...' + rule.program.substr(-30) : rule.program}</td>
|
||||
<td>${rule.protocol}</td>
|
||||
<td>${rule.localPort}</td>
|
||||
<td>${rule.remoteAddress}</td>
|
||||
<td>
|
||||
<span style="color: ${rule.action === 'allow' ? 'var(--success)' : 'var(--danger)'}">
|
||||
${rule.action === 'allow' ? '✓ 允许' : '✗ 阻止'}
|
||||
</span>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// 打开添加模态框
|
||||
function openAddModal() {
|
||||
currentEditId = null;
|
||||
document.getElementById('modal-title').textContent = '新建防火墙规则';
|
||||
document.getElementById('rule-modal').classList.add('active');
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 编辑规则
|
||||
function editRule(id) {
|
||||
currentEditId = id;
|
||||
const rule = rules.find(r => r.id === id);
|
||||
if (!rule) return;
|
||||
|
||||
document.getElementById('modal-title').textContent = '编辑防火墙规则';
|
||||
document.getElementById('rule-modal').classList.add('active');
|
||||
|
||||
// 填充表单
|
||||
document.getElementById('rule-name').value = rule.name;
|
||||
document.getElementById('rule-description').value = rule.description || '';
|
||||
document.getElementById('rule-direction').value = rule.direction;
|
||||
document.getElementById('rule-action').value = rule.action;
|
||||
document.getElementById('rule-enabled').checked = rule.enabled;
|
||||
document.getElementById('program-path').value = rule.program === '任意' ? '' : rule.program;
|
||||
|
||||
// 这里可以继续填充其他字段...
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal() {
|
||||
document.getElementById('rule-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
function switchTab(tabName) {
|
||||
// 更新标签按钮状态
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// 显示对应内容
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||||
}
|
||||
|
||||
// 切换端口字段显示
|
||||
function togglePortFields() {
|
||||
const protocol = document.getElementById('protocol-type').value;
|
||||
const portFields = document.getElementById('port-fields');
|
||||
const customProtocolGroup = document.getElementById('custom-protocol-group');
|
||||
|
||||
if (protocol === 'tcp' || protocol === 'udp') {
|
||||
portFields.style.display = 'grid';
|
||||
customProtocolGroup.style.display = 'none';
|
||||
} else if (protocol === 'custom') {
|
||||
portFields.style.display = 'none';
|
||||
customProtocolGroup.style.display = 'block';
|
||||
} else {
|
||||
portFields.style.display = 'none';
|
||||
customProtocolGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存规则
|
||||
function saveRule() {
|
||||
const name = document.getElementById('rule-name').value.trim();
|
||||
if (!name) {
|
||||
alert('请输入规则名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = {
|
||||
id: currentEditId || Date.now(),
|
||||
name: name,
|
||||
enabled: document.getElementById('rule-enabled').checked,
|
||||
direction: document.getElementById('rule-direction').value,
|
||||
program: document.getElementById('program-path').value || '任意',
|
||||
protocol: document.getElementById('protocol-type').value.toUpperCase(),
|
||||
localPort: document.getElementById('local-ports').value || '任意',
|
||||
remotePort: document.getElementById('remote-ports').value || '任意',
|
||||
remoteAddress: '任意', // 简化处理
|
||||
action: document.getElementById('rule-action').value,
|
||||
description: document.getElementById('rule-description').value
|
||||
};
|
||||
|
||||
if (currentEditId) {
|
||||
// 更新现有规则
|
||||
const index = rules.findIndex(r => r.id === currentEditId);
|
||||
if (index !== -1) {
|
||||
rules[index] = rule;
|
||||
}
|
||||
} else {
|
||||
// 添加新规则
|
||||
rules.unshift(rule);
|
||||
}
|
||||
|
||||
renderRules();
|
||||
closeModal();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
document.getElementById('rule-name').value = '';
|
||||
document.getElementById('rule-description').value = '';
|
||||
document.getElementById('rule-direction').value = 'inbound';
|
||||
document.getElementById('rule-action').value = 'allow';
|
||||
document.getElementById('rule-enabled').checked = true;
|
||||
document.getElementById('program-path').value = '';
|
||||
document.getElementById('protocol-type').value = 'any';
|
||||
document.getElementById('local-ports').value = '';
|
||||
document.getElementById('remote-ports').value = '';
|
||||
|
||||
// 重置到第一个标签页
|
||||
document.querySelectorAll('.tab').forEach((tab, index) => {
|
||||
tab.classList.toggle('active', index === 0);
|
||||
});
|
||||
document.querySelectorAll('.tab-content').forEach((content, index) => {
|
||||
content.classList.toggle('active', index === 0);
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤规则
|
||||
function filterRules(type) {
|
||||
// 实际实现中应该过滤规则
|
||||
console.log('过滤规则:', type);
|
||||
}
|
||||
|
||||
// 搜索规则
|
||||
function searchRules(keyword) {
|
||||
// 实际实现中应该搜索规则
|
||||
console.log('搜索规则:', keyword);
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
function updateStats() {
|
||||
document.getElementById('total-rules').textContent = rules.length;
|
||||
}
|
||||
|
||||
// 导出规则
|
||||
function exportRules() {
|
||||
const dataStr = JSON.stringify(rules, null, 2);
|
||||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'firewall_rules.json';
|
||||
link.click();
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
function importRules() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const importedRules = JSON.parse(e.target.result);
|
||||
rules = importedRules;
|
||||
renderRules();
|
||||
updateStats();
|
||||
alert('规则导入成功!');
|
||||
} catch (err) {
|
||||
alert('导入失败:文件格式错误');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
// 处理本地/远程IP选择
|
||||
document.querySelectorAll('input[name="local-ip"]').forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
document.getElementById('local-ip-list').style.display =
|
||||
this.value === 'custom' ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="remote-ip"]').forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
document.getElementById('remote-ip-list').style.display =
|
||||
this.value === 'custom' ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载时初始化
|
||||
window.onload = function() {
|
||||
renderRules();
|
||||
updateStats();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,1233 +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>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.3.4/vue.global.prod.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 24px 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #2d3748;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 20px 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select, .search-input {
|
||||
padding: 10px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.filter-select:focus, .search-input:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.rules-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rules-header {
|
||||
padding: 20px 32px;
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 100px 150px 120px 100px 140px;
|
||||
gap: 16px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 100px 150px 120px 100px 140px;
|
||||
gap: 16px;
|
||||
padding: 20px 32px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: all 0.3s ease;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-item:hover {
|
||||
background: rgba(79, 70, 229, 0.05);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.rule-program {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.action-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-allow {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.action-block {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.direction-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.direction-inbound {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.direction-outbound {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.icon-btn.edit {
|
||||
background: #f0f9ff;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.icon-btn.delete {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.icon-btn.toggle {
|
||||
background: #f7fee7;
|
||||
color: #65a30d;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #e2e8f0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.service-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24px 32px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #4f46e5;
|
||||
border-bottom-color: #4f46e5;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.rules-grid, .rule-item {
|
||||
grid-template-columns: 1fr 100px 80px 120px 100px 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rules-grid, .rule-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app-container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<h1>
|
||||
<i data-lucide="shield"></i>
|
||||
防火墙管理看板
|
||||
</h1>
|
||||
<div class="header-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ enabledRules }}</div>
|
||||
<div class="stat-label">已启用</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ disabledRules }}</div>
|
||||
<div class="stat-label">已禁用</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ totalRules }}</div>
|
||||
<div class="stat-label">总规则</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div class="filter-group">
|
||||
<select v-model="filterDirection" class="filter-select">
|
||||
<option value="">所有方向</option>
|
||||
<option value="inbound">入站</option>
|
||||
<option value="outbound">出站</option>
|
||||
</select>
|
||||
<select v-model="filterAction" class="filter-select">
|
||||
<option value="">所有操作</option>
|
||||
<option value="allow">允许</option>
|
||||
<option value="block">阻止</option>
|
||||
</select>
|
||||
<select v-model="filterStatus" class="filter-select">
|
||||
<option value="">所有状态</option>
|
||||
<option value="enabled">已启用</option>
|
||||
<option value="disabled">已禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索规则名称..."
|
||||
v-model="searchText"
|
||||
class="search-input"
|
||||
>
|
||||
<button class="btn btn-primary" @click="openModal()">
|
||||
<i data-lucide="plus"></i>
|
||||
新增规则
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules Container -->
|
||||
<div class="rules-container">
|
||||
<div class="rules-header">
|
||||
<div class="rules-grid">
|
||||
<div>规则名称</div>
|
||||
<div>方向</div>
|
||||
<div>操作</div>
|
||||
<div>协议/端口</div>
|
||||
<div>状态</div>
|
||||
<div>作用域</div>
|
||||
<div>操作</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="rule in filteredRules" :key="rule.id" class="rule-item">
|
||||
<div>
|
||||
<div class="rule-name">{{ rule.name }}</div>
|
||||
<div class="rule-program">{{ rule.program }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span :class="'direction-badge direction-' + rule.direction">
|
||||
{{ rule.direction === 'inbound' ? '入站' : '出站' }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span :class="'action-badge action-' + rule.action">
|
||||
{{ rule.action === 'allow' ? '允许' : '阻止' }}
|
||||
</span>
|
||||
</div>
|
||||
<div>{{ rule.protocol }}/{{ rule.ports }}</div>
|
||||
<div>
|
||||
<span :class="'status-badge status-' + rule.status">
|
||||
{{ rule.status === 'enabled' ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
<div>{{ rule.scope }}</div>
|
||||
<div class="rule-actions">
|
||||
<button class="icon-btn edit" @click="editRule(rule)" title="编辑">
|
||||
<i data-lucide="edit"></i>
|
||||
</button>
|
||||
<button class="icon-btn toggle" @click="toggleRule(rule)" title="切换状态">
|
||||
<i :data-lucide="rule.status === 'enabled' ? 'pause' : 'play'"></i>
|
||||
</button>
|
||||
<button class="icon-btn delete" @click="deleteRule(rule.id)" title="删除">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal" :class="{ show: showModal }" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{ editingRule ? '编辑规则' : '新增规则' }}</h2>
|
||||
<button class="close-btn" @click="closeModal">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="{ active: activeTab === 'general' }" @click="activeTab = 'general'">
|
||||
常规
|
||||
</div>
|
||||
<div class="tab" :class="{ active: activeTab === 'programs' }" @click="activeTab = 'programs'">
|
||||
程序和服务
|
||||
</div>
|
||||
<div class="tab" :class="{ active: activeTab === 'protocols' }" @click="activeTab = 'protocols'">
|
||||
协议和端口
|
||||
</div>
|
||||
<div class="tab" :class="{ active: activeTab === 'scope' }" @click="activeTab = 'scope'">
|
||||
作用域
|
||||
</div>
|
||||
<div class="tab" :class="{ active: activeTab === 'advanced' }" @click="activeTab = 'advanced'">
|
||||
高级
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常规选项卡 -->
|
||||
<div class="tab-content" :class="{ active: activeTab === 'general' }">
|
||||
<div class="form-group">
|
||||
<label class="form-label">规则名称</label>
|
||||
<input type="text" v-model="formData.name" class="form-input" placeholder="输入规则名称">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea v-model="formData.description" class="form-textarea" placeholder="规则描述(可选)"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">方向</label>
|
||||
<select v-model="formData.direction" class="form-select">
|
||||
<option value="inbound">入站</option>
|
||||
<option value="outbound">出站</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">操作</label>
|
||||
<select v-model="formData.action" class="form-select">
|
||||
<option value="allow">允许连接</option>
|
||||
<option value="block">阻止连接</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" v-model="formData.enabled" class="checkbox" id="enabled">
|
||||
<label for="enabled">启用此规则</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 程序和服务选项卡 -->
|
||||
<div class="tab-content" :class="{ active: activeTab === 'programs' }">
|
||||
<div class="form-group">
|
||||
<label class="form-label">应用程序</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.programType" value="all" name="programType" id="all-programs">
|
||||
<label for="all-programs">所有程序</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.programType" value="specific" name="programType" id="specific-program">
|
||||
<label for="specific-program">此程序路径:</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.programPath"
|
||||
class="form-input"
|
||||
placeholder="C:\Program Files\..."
|
||||
:disabled="formData.programType !== 'specific'"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">服务</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.serviceType" value="all" name="serviceType" id="all-services">
|
||||
<label for="all-services">应用于所有程序和服务</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.serviceType" value="specific" name="serviceType" id="specific-services">
|
||||
<label for="specific-services">仅应用于这些服务:</label>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.serviceType === 'specific'" class="service-list">
|
||||
<div v-for="service in availableServices" :key="service.name" class="service-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="service.name"
|
||||
v-model="formData.selectedServices"
|
||||
:id="'service-' + service.name"
|
||||
>
|
||||
<label :for="'service-' + service.name">{{ service.displayName }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 协议和端口选项卡 -->
|
||||
<div class="tab-content" :class="{ active: activeTab === 'protocols' }">
|
||||
<div class="form-group">
|
||||
<label class="form-label">协议类型</label>
|
||||
<select v-model="formData.protocolType" class="form-select">
|
||||
<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 v-if="formData.protocolType === 'custom'" class="form-group">
|
||||
<label class="form-label">协议号</label>
|
||||
<input type="number" v-model="formData.protocolNumber" class="form-input" placeholder="0-255">
|
||||
</div>
|
||||
|
||||
<div v-if="['tcp', 'udp'].includes(formData.protocolType)" class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">本地端口</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.localPortType" value="all" name="localPortType" id="all-local-ports">
|
||||
<label for="all-local-ports">所有端口</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.localPortType" value="specific" name="localPortType" id="specific-local-ports">
|
||||
<label for="specific-local-ports">特定端口:</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.localPorts"
|
||||
class="form-input"
|
||||
placeholder="80,443,8000-8080"
|
||||
:disabled="formData.localPortType !== 'specific'"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">远程端口</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.remotePortType" value="all" name="remotePortType" id="all-remote-ports">
|
||||
<label for="all-remote-ports">所有端口</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.remotePortType" value="specific" name="remotePortType" id="specific-remote-ports">
|
||||
<label for="specific-remote-ports">特定端口:</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.remotePorts"
|
||||
class="form-input"
|
||||
placeholder="80,443,8000-8080"
|
||||
:disabled="formData.remotePortType !== 'specific'"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作用域选项卡 -->
|
||||
<div class="tab-content" :class="{ active: activeTab === 'scope' }">
|
||||
<div class="form-group">
|
||||
<label class="form-label">本地IP地址</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.localAddressType" value="any" name="localAddressType" id="any-local-address">
|
||||
<label for="any-local-address">任何IP地址</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.localAddressType" value="specific" name="localAddressType" id="specific-local-address">
|
||||
<label for="specific-local-address">这些IP地址:</label>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="formData.localAddresses"
|
||||
class="form-textarea"
|
||||
placeholder="192.168.1.1 10.0.0.0/8 172.16.0.0-172.31.255.255"
|
||||
:disabled="formData.localAddressType !== 'specific'"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">远程IP地址</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.remoteAddressType" value="any" name="remoteAddressType" id="any-remote-address">
|
||||
<label for="any-remote-address">任何IP地址</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.remoteAddressType" value="specific" name="remoteAddressType" id="specific-remote-address">
|
||||
<label for="specific-remote-address">这些IP地址:</label>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="formData.remoteAddresses"
|
||||
class="form-textarea"
|
||||
placeholder="8.8.8.8 192.168.0.0/16 203.0.113.0-203.0.113.255"
|
||||
:disabled="formData.remoteAddressType !== 'specific'"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级选项卡 -->
|
||||
<div class="tab-content" :class="{ active: activeTab === 'advanced' }">
|
||||
<div class="form-group">
|
||||
<label class="form-label">配置文件</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" v-model="formData.profiles.domain" id="profile-domain">
|
||||
<label for="profile-domain">域</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" v-model="formData.profiles.private" id="profile-private">
|
||||
<label for="profile-private">专用</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" v-model="formData.profiles.public" id="profile-public">
|
||||
<label for="profile-public">公用</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">边缘遍历</label>
|
||||
<select v-model="formData.edgeTraversal" class="form-select">
|
||||
<option value="no">阻止边缘遍历</option>
|
||||
<option value="allow">允许边缘遍历</option>
|
||||
<option value="defer">延迟到用户</option>
|
||||
<option value="defer-app">延迟到应用程序</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">授权用户</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.userType" value="any" name="userType" id="any-user">
|
||||
<label for="any-user">任何用户</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="radio" v-model="formData.userType" value="specific" name="userType" id="specific-users">
|
||||
<label for="specific-users">仅这些用户:</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.authorizedUsers"
|
||||
class="form-input"
|
||||
placeholder="DOMAIN\Username, Everyone, ..."
|
||||
:disabled="formData.userType !== 'specific'"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button class="btn btn-success" @click="saveRule">
|
||||
{{ editingRule ? '保存' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
editingRule: null,
|
||||
activeTab: 'general',
|
||||
searchText: '',
|
||||
filterDirection: '',
|
||||
filterAction: '',
|
||||
filterStatus: '',
|
||||
|
||||
// 表单数据
|
||||
formData: {
|
||||
name: '',
|
||||
description: '',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
enabled: true,
|
||||
programType: 'all',
|
||||
programPath: '',
|
||||
serviceType: 'all',
|
||||
selectedServices: [],
|
||||
protocolType: 'any',
|
||||
protocolNumber: '',
|
||||
localPortType: 'all',
|
||||
localPorts: '',
|
||||
remotePortType: 'all',
|
||||
remotePorts: '',
|
||||
localAddressType: 'any',
|
||||
localAddresses: '',
|
||||
remoteAddressType: 'any',
|
||||
remoteAddresses: '',
|
||||
profiles: {
|
||||
domain: true,
|
||||
private: true,
|
||||
public: false
|
||||
},
|
||||
edgeTraversal: 'no',
|
||||
userType: 'any',
|
||||
authorizedUsers: ''
|
||||
},
|
||||
|
||||
// 可用服务列表
|
||||
availableServices: [
|
||||
{ name: 'http', displayName: 'HTTP (80)' },
|
||||
{ name: 'https', displayName: 'HTTPS (443)' },
|
||||
{ name: 'ftp', displayName: 'FTP (21)' },
|
||||
{ name: 'ssh', displayName: 'SSH (22)' },
|
||||
{ name: 'telnet', displayName: 'Telnet (23)' },
|
||||
{ name: 'smtp', displayName: 'SMTP (25)' },
|
||||
{ name: 'dns', displayName: 'DNS (53)' },
|
||||
{ name: 'dhcp', displayName: 'DHCP (67-68)' },
|
||||
{ name: 'pop3', displayName: 'POP3 (110)' },
|
||||
{ name: 'imap', displayName: 'IMAP (143)' },
|
||||
{ name: 'snmp', displayName: 'SNMP (161)' },
|
||||
{ name: 'ldap', displayName: 'LDAP (389)' },
|
||||
{ name: 'smb', displayName: 'SMB (445)' },
|
||||
{ name: 'rdp', displayName: 'RDP (3389)' }
|
||||
],
|
||||
|
||||
// 防火墙规则数据
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Core Networking - DNS (UDP-Out)',
|
||||
program: 'svchost.exe',
|
||||
direction: 'outbound',
|
||||
action: 'allow',
|
||||
protocol: 'UDP',
|
||||
ports: '53',
|
||||
status: 'enabled',
|
||||
scope: '任何'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Windows Remote Management (HTTP-In)',
|
||||
program: 'svchost.exe',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
protocol: 'TCP',
|
||||
ports: '5985',
|
||||
status: 'enabled',
|
||||
scope: '本地子网'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'File and Printer Sharing (SMB-In)',
|
||||
program: 'System',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
protocol: 'TCP',
|
||||
ports: '445',
|
||||
status: 'disabled',
|
||||
scope: '专用'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Remote Desktop - User Mode (TCP-In)',
|
||||
program: 'svchost.exe',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
protocol: 'TCP',
|
||||
ports: '3389',
|
||||
status: 'enabled',
|
||||
scope: '任何'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Block Suspicious Outbound',
|
||||
program: 'malware.exe',
|
||||
direction: 'outbound',
|
||||
action: 'block',
|
||||
protocol: 'TCP',
|
||||
ports: '任何',
|
||||
status: 'enabled',
|
||||
scope: '任何'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Web Browser (HTTP/HTTPS-Out)',
|
||||
program: 'chrome.exe',
|
||||
direction: 'outbound',
|
||||
action: 'allow',
|
||||
protocol: 'TCP',
|
||||
ports: '80,443',
|
||||
status: 'enabled',
|
||||
scope: '任何'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Email Client (SMTP-Out)',
|
||||
program: 'outlook.exe',
|
||||
direction: 'outbound',
|
||||
action: 'allow',
|
||||
protocol: 'TCP',
|
||||
ports: '25,587',
|
||||
status: 'disabled',
|
||||
scope: '任何'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Windows Update (HTTP-Out)',
|
||||
program: 'svchost.exe',
|
||||
direction: 'outbound',
|
||||
action: 'allow',
|
||||
protocol: 'TCP',
|
||||
ports: '80,443',
|
||||
status: 'enabled',
|
||||
scope: '任何'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredRules() {
|
||||
let filtered = this.rules;
|
||||
|
||||
if (this.searchText) {
|
||||
filtered = filtered.filter(rule =>
|
||||
rule.name.toLowerCase().includes(this.searchText.toLowerCase()) ||
|
||||
rule.program.toLowerCase().includes(this.searchText.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (this.filterDirection) {
|
||||
filtered = filtered.filter(rule => rule.direction === this.filterDirection);
|
||||
}
|
||||
|
||||
if (this.filterAction) {
|
||||
filtered = filtered.filter(rule => rule.action === this.filterAction);
|
||||
}
|
||||
|
||||
if (this.filterStatus) {
|
||||
filtered = filtered.filter(rule => rule.status === this.filterStatus);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
totalRules() {
|
||||
return this.rules.length;
|
||||
},
|
||||
|
||||
enabledRules() {
|
||||
return this.rules.filter(rule => rule.status === 'enabled').length;
|
||||
},
|
||||
|
||||
disabledRules() {
|
||||
return this.rules.filter(rule => rule.status === 'disabled').length;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openModal(rule = null) {
|
||||
this.editingRule = rule;
|
||||
this.activeTab = 'general';
|
||||
|
||||
if (rule) {
|
||||
// 编辑模式 - 填充表单数据
|
||||
this.formData = {
|
||||
name: rule.name,
|
||||
description: '',
|
||||
direction: rule.direction,
|
||||
action: rule.action,
|
||||
enabled: rule.status === 'enabled',
|
||||
programType: 'all',
|
||||
programPath: rule.program,
|
||||
serviceType: 'all',
|
||||
selectedServices: [],
|
||||
protocolType: rule.protocol.toLowerCase(),
|
||||
protocolNumber: '',
|
||||
localPortType: 'specific',
|
||||
localPorts: rule.ports,
|
||||
remotePortType: 'all',
|
||||
remotePorts: '',
|
||||
localAddressType: 'any',
|
||||
localAddresses: '',
|
||||
remoteAddressType: 'any',
|
||||
remoteAddresses: '',
|
||||
profiles: {
|
||||
domain: true,
|
||||
private: true,
|
||||
public: false
|
||||
},
|
||||
edgeTraversal: 'no',
|
||||
userType: 'any',
|
||||
authorizedUsers: ''
|
||||
};
|
||||
} else {
|
||||
// 新增模式 - 重置表单
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.editingRule = null;
|
||||
this.resetForm();
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.formData = {
|
||||
name: '',
|
||||
description: '',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
enabled: true,
|
||||
programType: 'all',
|
||||
programPath: '',
|
||||
serviceType: 'all',
|
||||
selectedServices: [],
|
||||
protocolType: 'any',
|
||||
protocolNumber: '',
|
||||
localPortType: 'all',
|
||||
localPorts: '',
|
||||
remotePortType: 'all',
|
||||
remotePorts: '',
|
||||
localAddressType: 'any',
|
||||
localAddresses: '',
|
||||
remoteAddressType: 'any',
|
||||
remoteAddresses: '',
|
||||
profiles: {
|
||||
domain: true,
|
||||
private: true,
|
||||
public: false
|
||||
},
|
||||
edgeTraversal: 'no',
|
||||
userType: 'any',
|
||||
authorizedUsers: ''
|
||||
};
|
||||
},
|
||||
|
||||
saveRule() {
|
||||
if (!this.formData.name.trim()) {
|
||||
alert('请输入规则名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const ruleData = {
|
||||
name: this.formData.name,
|
||||
program: this.formData.programType === 'specific' ? this.formData.programPath : 'System',
|
||||
direction: this.formData.direction,
|
||||
action: this.formData.action,
|
||||
protocol: this.formData.protocolType.toUpperCase(),
|
||||
ports: this.formData.localPortType === 'specific' ? this.formData.localPorts : '任何',
|
||||
status: this.formData.enabled ? 'enabled' : 'disabled',
|
||||
scope: '任何'
|
||||
};
|
||||
|
||||
if (this.editingRule) {
|
||||
// 编辑现有规则
|
||||
const index = this.rules.findIndex(rule => rule.id === this.editingRule.id);
|
||||
if (index !== -1) {
|
||||
this.rules[index] = { ...this.rules[index], ...ruleData };
|
||||
}
|
||||
} else {
|
||||
// 新增规则
|
||||
const newRule = {
|
||||
id: Math.max(...this.rules.map(r => r.id)) + 1,
|
||||
...ruleData
|
||||
};
|
||||
this.rules.push(newRule);
|
||||
}
|
||||
|
||||
this.closeModal();
|
||||
},
|
||||
|
||||
editRule(rule) {
|
||||
this.openModal(rule);
|
||||
},
|
||||
|
||||
toggleRule(rule) {
|
||||
rule.status = rule.status === 'enabled' ? 'disabled' : 'enabled';
|
||||
},
|
||||
|
||||
deleteRule(ruleId) {
|
||||
if (confirm('确定要删除这个规则吗?')) {
|
||||
this.rules = this.rules.filter(rule => rule.id !== ruleId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 初始化Lucide图标
|
||||
lucide.createIcons();
|
||||
},
|
||||
|
||||
updated() {
|
||||
// 更新时重新创建图标
|
||||
lucide.createIcons();
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,10 +14,15 @@ import { useUuidStore } from '@/stores/uuidStore'
|
||||
import { logB } from '@/utils/logHelper/logUtils'
|
||||
import { httpConfig } from '@/config/httpConfig'
|
||||
|
||||
// Vue Virtual Scroller
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(websocketPlugin)
|
||||
app.use(VueVirtualScroller)
|
||||
// 使用 主题 支持
|
||||
app.use(createNaiveUI())
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
import type { arrayUtilsNotEmptyCallback } from '@/types/utils'
|
||||
|
||||
export default class ArrayUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 指定数组或未知元素是否为 `null` 或 `empty`
|
||||
* @param arr
|
||||
*/
|
||||
static isEmpty (arr: any[] | null): boolean {
|
||||
static isEmpty (arr: unknown[] | null): boolean {
|
||||
return arr === null || arr.length === 0
|
||||
}
|
||||
}
|
||||
@@ -17,28 +17,20 @@
|
||||
<template #icon>
|
||||
<n-icon :component="AddOutline" />
|
||||
</template>
|
||||
新建规则
|
||||
<span class="button-text">新建规则</span>
|
||||
</n-button>
|
||||
<n-dropdown :options="batchOptions" placement="bottom-end">
|
||||
<n-button size="medium" ghost>
|
||||
批量操作
|
||||
<template #icon>
|
||||
<n-icon :component="ChevronDownOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规则统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<!-- 规则统计卡片 - 响应式隐藏 -->
|
||||
<div class="stats-grid stats-hidden-on-small">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper primary">
|
||||
<n-icon :component="ListOutline" size="24" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.total }}</div>
|
||||
<div class="stat-label">总规则数</div>
|
||||
<div class="stat-label">禁用规则</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +50,7 @@
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.inactive }}</div>
|
||||
<div class="stat-label">禁用规则</div>
|
||||
<div class="stat-label">出站规则</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +60,7 @@
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.blocked }}</div>
|
||||
<div class="stat-label">阻止连接</div>
|
||||
<div class="stat-label">入站规则</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +73,6 @@
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索规则名称..."
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
@@ -89,57 +80,195 @@
|
||||
<n-icon :component="SearchOutline" />
|
||||
</template>
|
||||
</n-input>
|
||||
<n-dropdown :options="filterDropdownOptions" placement="bottom-end">
|
||||
<n-button size="small" ghost>
|
||||
<template #icon>
|
||||
<n-icon :component="FilterOutline" />
|
||||
</template>
|
||||
筛选
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="filteredRules"
|
||||
:pagination="false"
|
||||
:scroll-x="1200"
|
||||
:virtual-scroll="true"
|
||||
:max-height="600"
|
||||
size="small"
|
||||
striped
|
||||
:row-key="(row) => row.id"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
<!-- 优化后的筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">方向</span>
|
||||
<n-select
|
||||
v-model:value="filters.direction"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
:options="directionOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">操作</span>
|
||||
<n-select
|
||||
v-model:value="filters.action"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
:options="actionOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">状态</span>
|
||||
<n-select
|
||||
v-model:value="filters.enabled"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
:options="enabledOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">协议</span>
|
||||
<n-select
|
||||
v-model:value="filters.protocol"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
:options="protocolOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<n-button size="small" quaternary @click="clearFilters">
|
||||
<template #icon>
|
||||
<n-icon :component="RefreshOutline" />
|
||||
</template>
|
||||
重置筛选
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 虚拟滚动规则列表 -->
|
||||
<div class="rules-list-container">
|
||||
<!-- 列表头部 -->
|
||||
<div class="list-header">
|
||||
<div class="header-cell checkbox-cell">
|
||||
<n-checkbox
|
||||
v-model:checked="allSelected"
|
||||
:indeterminate="indeterminate"
|
||||
@update:checked="handleSelectAll"
|
||||
/>
|
||||
</div>
|
||||
<div class="header-cell name-cell">规则名称</div>
|
||||
<div class="header-cell status-cell">状态</div>
|
||||
<div class="header-cell direction-cell">方向</div>
|
||||
<div class="header-cell action-cell">操作</div>
|
||||
<div class="header-cell program-cell">程序/服务</div>
|
||||
<div class="header-cell protocol-cell">协议</div>
|
||||
<div class="header-cell port-cell">端口</div>
|
||||
<div class="header-cell profiles-cell">配置文件</div>
|
||||
<div class="header-cell actions-cell">操作</div>
|
||||
</div>
|
||||
|
||||
<!-- 虚拟滚动列表 -->
|
||||
<RecycleScroller
|
||||
class="rules-scroller"
|
||||
:items="filteredRules"
|
||||
:item-size="80"
|
||||
key-field="id"
|
||||
v-slot="{ item }"
|
||||
>
|
||||
<div class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
|
||||
<div class="rule-cell checkbox-cell">
|
||||
<n-checkbox
|
||||
:checked="checkedRowKeys.includes(item.id)"
|
||||
@update:checked="(checked) => handleRuleCheck(item.id, checked)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell name-cell">
|
||||
<div class="rule-name">{{ item.name }}</div>
|
||||
<div class="rule-description">{{ item.description }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell status-cell">
|
||||
<n-tag :type="item.enabled ? 'success' : 'default'" size="small">
|
||||
{{ item.enabled ? '启用' : '禁用' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell direction-cell">
|
||||
<div class="direction-content">
|
||||
<n-icon
|
||||
:component="item.direction === 'inbound' ? ArrowDownOutline : ArrowUpOutline"
|
||||
size="16"
|
||||
/>
|
||||
<span>{{ item.direction === 'inbound' ? '入站' : '出站' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell action-cell">
|
||||
<n-tag :type="item.action === 'allow' ? 'success' : 'error'" size="small">
|
||||
{{ item.action === 'allow' ? '允许' : '阻止' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell program-cell">
|
||||
<div class="program-text" :title="item.program">{{ item.program }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell protocol-cell">
|
||||
<span>{{ item.protocol }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell port-cell">
|
||||
<span>{{ item.port }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell profiles-cell">
|
||||
<div class="profiles-container">
|
||||
<n-tag
|
||||
v-for="profile in item.profiles"
|
||||
:key="profile"
|
||||
type="info"
|
||||
size="small"
|
||||
class="profile-tag"
|
||||
>
|
||||
{{ profile }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-cell actions-cell">
|
||||
<div class="action-buttons">
|
||||
<n-button size="small" quaternary @click="editRule(item)">
|
||||
<template #icon>
|
||||
<n-icon :component="CreateOutline" size="14" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="small" quaternary type="error" @click="deleteRule(item.id)">
|
||||
<template #icon>
|
||||
<n-icon :component="TrashOutline" size="14" />
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防火墙规则表单 -->
|
||||
<FirewallRuleForm
|
||||
v-model="showRuleForm"
|
||||
:edit-rule="currentEditRule"
|
||||
@save="handleSaveRule"
|
||||
/>
|
||||
<FirewallRuleForm v-model="showRuleForm" :edit-rule="currentEditRule" @save="handleSaveRule" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NIcon,
|
||||
NInput,
|
||||
NSwitch,
|
||||
NTag,
|
||||
NDataTable,
|
||||
import {
|
||||
NButton,
|
||||
NIcon,
|
||||
NInput,
|
||||
NSwitch,
|
||||
NTag,
|
||||
NDropdown,
|
||||
NCheckbox,
|
||||
NSelect,
|
||||
useMessage,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import FirewallRuleForm from '@/components/FirewallRuleForm.vue'
|
||||
import {
|
||||
AddOutline,
|
||||
@@ -149,12 +278,11 @@ import {
|
||||
CloseCircleOutline,
|
||||
ListOutline,
|
||||
ChevronDownOutline,
|
||||
FilterOutline,
|
||||
CreateOutline,
|
||||
CopyOutline,
|
||||
TrashOutline,
|
||||
ArrowUpOutline,
|
||||
ArrowDownOutline,
|
||||
RefreshOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -167,55 +295,52 @@ const checkedRowKeys = ref<string[]>([])
|
||||
|
||||
// 搜索和过滤
|
||||
const searchQuery = ref('')
|
||||
const filters = ref({
|
||||
direction: null as string | null,
|
||||
action: null as string | null,
|
||||
enabled: null as boolean | null,
|
||||
protocol: null as string | null,
|
||||
})
|
||||
|
||||
// 过滤选项
|
||||
const directionOptions = [
|
||||
{ label: '入站', value: 'inbound' },
|
||||
{ label: '出站', value: 'outbound' },
|
||||
]
|
||||
|
||||
const actionOptions = [
|
||||
{ label: '允许', value: 'allow' },
|
||||
{ label: '阻止', value: 'block' },
|
||||
]
|
||||
|
||||
const enabledOptions = [
|
||||
{ label: '已启用', value: true },
|
||||
{ label: '已禁用', value: false },
|
||||
]
|
||||
|
||||
const protocolOptions = [
|
||||
{ label: 'TCP', value: 'TCP' },
|
||||
{ label: 'UDP', value: 'UDP' },
|
||||
{ label: '任意', value: '任意' },
|
||||
]
|
||||
|
||||
// 批量操作选项
|
||||
const batchOptions = [
|
||||
{
|
||||
label: '启用选中',
|
||||
key: 'enable',
|
||||
icon: () => h(NIcon, { component: CheckmarkCircleOutline })
|
||||
icon: () => h(NIcon, { component: CheckmarkCircleOutline }),
|
||||
},
|
||||
{
|
||||
label: '禁用选中',
|
||||
label: '禁用选中',
|
||||
key: 'disable',
|
||||
icon: () => h(NIcon, { component: PauseCircleOutline })
|
||||
icon: () => h(NIcon, { component: PauseCircleOutline }),
|
||||
},
|
||||
{
|
||||
label: '删除选中',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, { component: TrashOutline })
|
||||
}
|
||||
]
|
||||
|
||||
// 筛选下拉选项
|
||||
const filterDropdownOptions = [
|
||||
{
|
||||
label: '方向',
|
||||
key: 'direction-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '入站规则', key: 'inbound' },
|
||||
{ label: '出站规则', key: 'outbound' }
|
||||
]
|
||||
icon: () => h(NIcon, { component: TrashOutline }),
|
||||
},
|
||||
{
|
||||
label: '操作',
|
||||
key: 'action-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '允许', key: 'allow' },
|
||||
{ label: '阻止', key: 'block' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
key: 'status-header',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ label: '已启用', key: 'enabled' },
|
||||
{ label: '已禁用', key: 'disabled' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 统计数据
|
||||
@@ -242,7 +367,7 @@ const mockRules = ref([
|
||||
priority: 100,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
id: '2',
|
||||
name: 'Microsoft Edge',
|
||||
description: '允许浏览器访问网络',
|
||||
enabled: true,
|
||||
@@ -262,7 +387,7 @@ const mockRules = ref([
|
||||
direction: 'inbound',
|
||||
action: 'block',
|
||||
program: '任意',
|
||||
protocol: 'TCP',
|
||||
protocol: 'TCP',
|
||||
port: '23',
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 300,
|
||||
@@ -285,7 +410,7 @@ const mockRules = ref([
|
||||
name: 'HTTP 服务器',
|
||||
description: '允许 Web 服务器接收请求',
|
||||
enabled: true,
|
||||
direction: 'inbound',
|
||||
direction: 'inbound',
|
||||
action: 'allow',
|
||||
program: '任意',
|
||||
protocol: 'TCP',
|
||||
@@ -293,172 +418,126 @@ const mockRules = ref([
|
||||
profiles: ['域', '专用', '公用'],
|
||||
priority: 500,
|
||||
},
|
||||
// 添加更多模拟数据以测试虚拟滚动
|
||||
...Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `${i + 6}`,
|
||||
name: `规则 ${i + 6}`,
|
||||
description: `这是第 ${i + 6} 个测试规则`,
|
||||
enabled: Math.random() > 0.5,
|
||||
direction: Math.random() > 0.5 ? 'inbound' : 'outbound',
|
||||
action: Math.random() > 0.5 ? 'allow' : 'block',
|
||||
program: `程序 ${i + 6}`,
|
||||
protocol: ['TCP', 'UDP', '任意'][Math.floor(Math.random() * 3)],
|
||||
port: Math.floor(Math.random() * 65535).toString(),
|
||||
profiles: ['域', '专用', '公用'].slice(0, Math.floor(Math.random() * 3) + 1),
|
||||
priority: (i + 6) * 100,
|
||||
})),
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns: DataTableColumns = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 40
|
||||
},
|
||||
{
|
||||
title: '规则名称',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'rule-name-cell' }, [
|
||||
h('div', { class: 'rule-name' }, row.name),
|
||||
h('div', { class: 'rule-description' }, row.description)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enabled',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h(NTag, {
|
||||
type: row.enabled ? 'success' : 'default',
|
||||
size: 'small'
|
||||
}, {
|
||||
default: () => row.enabled ? '启用' : '禁用'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
key: 'direction',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'direction-cell' }, [
|
||||
h(NIcon, {
|
||||
component: row.direction === 'inbound' ? ArrowDownOutline : ArrowUpOutline,
|
||||
size: 16,
|
||||
style: { marginRight: '4px' }
|
||||
}),
|
||||
row.direction === 'inbound' ? '入站' : '出站'
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 80,
|
||||
render(row: any) {
|
||||
return h(NTag, {
|
||||
type: row.action === 'allow' ? 'success' : 'error',
|
||||
size: 'small'
|
||||
}, {
|
||||
default: () => row.action === 'allow' ? '允许' : '阻止'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '程序/服务',
|
||||
key: 'program',
|
||||
width: 250,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '协议',
|
||||
key: 'protocol',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '配置文件',
|
||||
key: 'profiles',
|
||||
width: 150,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'profiles-cell' },
|
||||
row.profiles.map((profile: string) =>
|
||||
h(NTag, {
|
||||
key: profile,
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
style: { marginRight: '4px', marginBottom: '2px' }
|
||||
}, {
|
||||
default: () => profile
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render(row: any) {
|
||||
return h('div', { class: 'action-buttons' }, [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
onClick: () => editRule(row)
|
||||
}, {
|
||||
default: () => '编辑'
|
||||
}),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
type: 'error',
|
||||
onClick: () => deleteRule(row.id)
|
||||
}, {
|
||||
default: () => '删除'
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredRules = computed(() => {
|
||||
let rules = mockRules.value
|
||||
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
rules = rules.filter(rule =>
|
||||
rule.name.toLowerCase().includes(query) ||
|
||||
rule.description.toLowerCase().includes(query)
|
||||
rules = rules.filter(
|
||||
(rule) =>
|
||||
rule.name.toLowerCase().includes(query) || rule.description.toLowerCase().includes(query),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// 方向过滤
|
||||
if (filters.value.direction) {
|
||||
rules = rules.filter((rule) => rule.direction === filters.value.direction)
|
||||
}
|
||||
|
||||
// 操作过滤
|
||||
if (filters.value.action) {
|
||||
rules = rules.filter((rule) => rule.action === filters.value.action)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (filters.value.enabled !== null) {
|
||||
rules = rules.filter((rule) => rule.enabled === filters.value.enabled)
|
||||
}
|
||||
|
||||
// 协议过滤
|
||||
if (filters.value.protocol) {
|
||||
rules = rules.filter((rule) => rule.protocol === filters.value.protocol)
|
||||
}
|
||||
|
||||
return rules
|
||||
})
|
||||
|
||||
// 全选相关计算属性
|
||||
const allSelected = computed({
|
||||
get: () => {
|
||||
return (
|
||||
filteredRules.value.length > 0 && checkedRowKeys.value.length === filteredRules.value.length
|
||||
)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
checkedRowKeys.value = filteredRules.value.map((rule) => rule.id)
|
||||
} else {
|
||||
checkedRowKeys.value = []
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const indeterminate = computed(() => {
|
||||
return checkedRowKeys.value.length > 0 && checkedRowKeys.value.length < filteredRules.value.length
|
||||
})
|
||||
|
||||
// 方法
|
||||
const openRuleForm = (rule: any = null) => {
|
||||
const openRuleForm = (rule: unknown = null) => {
|
||||
currentEditRule.value = rule
|
||||
showRuleForm.value = true
|
||||
}
|
||||
|
||||
const editRule = (rule: any) => {
|
||||
const editRule = (rule: unknown) => {
|
||||
currentEditRule.value = { ...rule }
|
||||
showRuleForm.value = true
|
||||
}
|
||||
|
||||
const deleteRule = (id: string) => {
|
||||
const index = mockRules.value.findIndex(rule => rule.id === id)
|
||||
const index = mockRules.value.findIndex((rule) => rule.id === id)
|
||||
if (index !== -1) {
|
||||
mockRules.value.splice(index, 1)
|
||||
// 从选中项中移除
|
||||
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
|
||||
message.success('规则删除成功')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheck = (keys: string[]) => {
|
||||
checkedRowKeys.value = keys
|
||||
const handleRuleCheck = (id: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!checkedRowKeys.value.includes(id)) {
|
||||
checkedRowKeys.value.push(id)
|
||||
}
|
||||
} else {
|
||||
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveRule = (rule: any) => {
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
allSelected.value = checked
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
filters.value = {
|
||||
direction: null,
|
||||
action: null,
|
||||
enabled: null,
|
||||
protocol: null,
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveRule = (rule: unknown) => {
|
||||
if (rule.id) {
|
||||
// 编辑现有规则
|
||||
const index = mockRules.value.findIndex(r => r.id === rule.id)
|
||||
const index = mockRules.value.findIndex((r) => r.id === rule.id)
|
||||
if (index !== -1) {
|
||||
mockRules.value[index] = rule
|
||||
message.success('规则更新成功')
|
||||
@@ -469,7 +548,7 @@ const handleSaveRule = (rule: any) => {
|
||||
mockRules.value.unshift(rule)
|
||||
message.success('规则创建成功')
|
||||
}
|
||||
|
||||
|
||||
// 重置状态
|
||||
currentEditRule.value = null
|
||||
}
|
||||
@@ -477,15 +556,21 @@ const handleSaveRule = (rule: any) => {
|
||||
|
||||
<style scoped>
|
||||
.firewall-view {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.firewall-container {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0; /* 允许容器缩小 */
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
@@ -493,12 +578,15 @@ const handleSaveRule = (rule: any) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 24px;
|
||||
margin-bottom: 8px;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0; /* 允许子元素缩小 */
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
flex: 1;
|
||||
min-width: 0; /* 允许内容缩小 */
|
||||
}
|
||||
|
||||
.view-title {
|
||||
@@ -506,18 +594,26 @@ const handleSaveRule = (rule: any) => {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.view-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 4px 0 0 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0; /* 允许内容缩小 */
|
||||
}
|
||||
|
||||
.firewall-status {
|
||||
@@ -528,6 +624,8 @@ const handleSaveRule = (rule: any) => {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
@@ -536,12 +634,17 @@ const handleSaveRule = (rule: any) => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -549,7 +652,7 @@ const handleSaveRule = (rule: any) => {
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -568,6 +671,7 @@ const handleSaveRule = (rule: any) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper.primary {
|
||||
@@ -592,6 +696,7 @@ const handleSaveRule = (rule: any) => {
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
@@ -614,14 +719,22 @@ const handleSaveRule = (rule: any) => {
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
margin-bottom: 50px;
|
||||
min-width: 0; /* 允许面板缩小 */
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 20px 24px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
@@ -629,23 +742,206 @@ const handleSaveRule = (rule: any) => {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
padding: 16px 24px 24px;
|
||||
/* 优化后的筛选区域 */
|
||||
.filter-section {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
background: var(--bg-tertiary);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 表格单元格样式 */
|
||||
.rule-name-cell {
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
/* 规则列表容器 */
|
||||
.rules-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0; /* 允许容器缩小 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 列表头部 */
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 40px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
min-width: 0; /* 允许头部缩小 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 基础列宽 - 针对超大屏幕(>1400px) */
|
||||
@media (min-width: 1401px) {
|
||||
.checkbox-cell { width: 50px; }
|
||||
.name-cell { width: 200px; }
|
||||
.status-cell { width: 80px; }
|
||||
.direction-cell { width: 80px; }
|
||||
.action-cell { width: 80px; }
|
||||
.program-cell { width: 250px; flex-shrink: 1; }
|
||||
.protocol-cell { width: 80px; }
|
||||
.port-cell { width: 100px; }
|
||||
.profiles-cell { width: 150px; flex-shrink: 1; }
|
||||
.actions-cell { width: 100px; }
|
||||
}
|
||||
|
||||
/* 大屏幕 1201px-1400px */
|
||||
@media (min-width: 1201px) and (max-width: 1400px) {
|
||||
.checkbox-cell { width: 45px; }
|
||||
.name-cell { width: 180px; }
|
||||
.status-cell { width: 70px; }
|
||||
.direction-cell { width: 70px; }
|
||||
.action-cell { width: 70px; }
|
||||
.program-cell { width: 220px; flex-shrink: 1; }
|
||||
.protocol-cell { width: 70px; }
|
||||
.port-cell { width: 90px; }
|
||||
.profiles-cell { width: 130px; flex-shrink: 1; }
|
||||
.actions-cell { width: 90px; }
|
||||
}
|
||||
|
||||
/* 中等屏幕 1025px-1200px */
|
||||
@media (min-width: 1025px) and (max-width: 1200px) {
|
||||
.checkbox-cell { width: 40px; }
|
||||
.name-cell { width: 160px; }
|
||||
.status-cell { width: 65px; }
|
||||
.direction-cell { width: 65px; }
|
||||
.action-cell { width: 65px; }
|
||||
.program-cell { width: 200px; flex-shrink: 1; }
|
||||
.protocol-cell { width: 65px; }
|
||||
.port-cell { width: 80px; }
|
||||
.profiles-cell { width: 120px; flex-shrink: 1; }
|
||||
.actions-cell { width: 85px; }
|
||||
}
|
||||
|
||||
/* 小屏幕 769px-1024px */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.checkbox-cell { width: 40px; }
|
||||
.name-cell { width: 140px; }
|
||||
.status-cell { width: 60px; }
|
||||
.direction-cell { width: 60px; }
|
||||
.action-cell { width: 60px; }
|
||||
.program-cell { width: 180px; flex-shrink: 1; }
|
||||
.protocol-cell { width: 60px; }
|
||||
.port-cell { width: 80px; }
|
||||
.profiles-cell { width: 100px; flex-shrink: 1; }
|
||||
.actions-cell { width: 80px; }
|
||||
}
|
||||
|
||||
/* 虚拟滚动列表 */
|
||||
.rules-scroller {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 80px;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
transition: var(--transition);
|
||||
background: var(--bg-card);
|
||||
min-width: 0; /* 允许项目缩小 */
|
||||
}
|
||||
|
||||
.rule-item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.rule-item.selected {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
/* 斑马纹效果 */
|
||||
.rule-item:nth-child(even) {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.rule-item:nth-child(odd) {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
/* 暗色模式下的斑马纹效果 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.rule-item:nth-child(even) {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
.rule-item:hover {
|
||||
background: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
.rule-item.selected {
|
||||
background: rgba(59, 130, 246, 0.05) !important;
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.rule-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
min-height: 60px;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
min-width: 0; /* 允许单元格缩小 */
|
||||
}
|
||||
|
||||
.rule-cell.name-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -653,111 +949,177 @@ const handleSaveRule = (rule: any) => {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rule-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.direction-cell {
|
||||
.direction-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
color: var(--text-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profiles-cell {
|
||||
.direction-content span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.program-text {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.profiles-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
/* 响应式设计 */
|
||||
|
||||
/* 在小屏幕上隐藏统计卡片 */
|
||||
@media (max-width: 1024px), (max-height: 700px) {
|
||||
.stats-hidden-on-small {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部工具栏响应式 */
|
||||
@media (max-width: 900px) {
|
||||
.firewall-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.view-subtitle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.status-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.firewall-status {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕特殊处理 */
|
||||
@media (max-width: 768px) {
|
||||
.firewall-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
.panel-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
.filter-group {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
min-width: 35px;
|
||||
}
|
||||
|
||||
/* 隐藏部分列 */
|
||||
.program-cell,
|
||||
.profiles-cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox-cell { width: 35px; }
|
||||
.name-cell { width: 120px; }
|
||||
.status-cell { width: 50px; }
|
||||
.direction-cell { width: 50px; }
|
||||
.action-cell { width: 50px; }
|
||||
.protocol-cell { width: 50px; }
|
||||
.port-cell { width: 60px; }
|
||||
.actions-cell { width: 70px; }
|
||||
}
|
||||
|
||||
/* 高度不足时的优化 */
|
||||
@media (max-height: 600px) {
|
||||
.firewall-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
.view-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.view-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.firewall-toolbar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rules-panel {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.firewall-status {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
.filter-section {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Naive UI 数据表格样式覆盖 */
|
||||
:deep(.n-data-table) {
|
||||
--n-td-color: var(--bg-card);
|
||||
--n-td-color-striped: var(--bg-tertiary);
|
||||
--n-th-color: var(--bg-secondary);
|
||||
--n-border-color: var(--border-secondary);
|
||||
--n-th-text-color: var(--text-secondary);
|
||||
--n-td-text-color: var(--text-primary);
|
||||
--n-font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-th) {
|
||||
font-weight: 600;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
:deep(.n-data-table-td) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-th) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* Naive UI 组件样式覆盖 */
|
||||
:deep(.n-input) {
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
@@ -766,6 +1128,14 @@ const handleSaveRule = (rule: any) => {
|
||||
--n-text-color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
--n-color: var(--bg-card);
|
||||
--n-border: 1px solid var(--border-tertiary);
|
||||
--n-border-hover: 1px solid var(--border-hover);
|
||||
--n-border-focus: 1px solid var(--accent-primary);
|
||||
--n-text-color: var(--text-primary);
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-menu) {
|
||||
--n-color: var(--bg-card);
|
||||
--n-option-text-color: var(--text-primary);
|
||||
@@ -779,4 +1149,17 @@ const handleSaveRule = (rule: any) => {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-checkbox) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* vue-virtual-scroller 样式 */
|
||||
:deep(.vue-recycle-scroller) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.vue-recycle-scroller__item-wrapper) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -573,7 +573,7 @@ const stats = computed(() => {
|
||||
|
||||
// 数据处理 - 将 ConnectionsTable1 的数据转换为 UnifiedConnectionsList4 的格式
|
||||
const originalItems = computed(() => {
|
||||
const result: any[] = []
|
||||
const result:unknown[] = []
|
||||
|
||||
if (!props.connections || !Array.isArray(props.connections) || props.connections.length === 0) {
|
||||
return result
|
||||
@@ -629,8 +629,8 @@ const originalItems = computed(() => {
|
||||
|
||||
// 显示数据
|
||||
const displayItems = computed(() => {
|
||||
const result: any[] = []
|
||||
const processGroups: { [key: number]: any[] } = {}
|
||||
const result:unknown[] = []
|
||||
const processGroups: { [key: number]:unknown[] } = {}
|
||||
|
||||
originalItems.value.forEach((item) => {
|
||||
const processIndex = item.processIndex
|
||||
@@ -701,7 +701,7 @@ const shouldShowSticky = computed(() => {
|
||||
return currentIndex > stickyIndex
|
||||
})
|
||||
|
||||
const shouldHideItem = (item: any, index: number) => {
|
||||
const shouldHideItem = (item:unknown, index: number) => {
|
||||
if (!stickyItem.value || !shouldShowSticky.value) return false
|
||||
return item.key === stickyItem.value.key
|
||||
}
|
||||
@@ -883,9 +883,9 @@ const getTimeSinceActive = (lastActive: Date): string => {
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
}
|
||||
|
||||
const getProcessStatus = (process: any) => {
|
||||
const getProcessStatus = (process:unknown) => {
|
||||
if (process.hasExited) return { type: 'error' as const, text: '已退出', show: true }
|
||||
const hasActiveConnections = process.connections?.some((c: any) => c.isActive)
|
||||
const hasActiveConnections = process.connections?.some((c:unknown) => c.isActive)
|
||||
if (hasActiveConnections) {
|
||||
return { type: 'success' as const, text: '活跃', show: true }
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ let scrollTimer: NodeJS.Timeout | null = null
|
||||
// 工具函数
|
||||
const debounce = (func: Function, wait: number) => {
|
||||
let timeout: NodeJS.Timeout
|
||||
return function executedFunction(...args: any[]) {
|
||||
return function executedFunction(...args:unknown[]) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
@@ -220,7 +220,7 @@ const throttle = (func: Function, delay: number) => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
let lastExecTime = 0
|
||||
|
||||
return function (...args: any[]) {
|
||||
return function (...args:unknown[]) {
|
||||
const currentTime = Date.now()
|
||||
|
||||
if (currentTime - lastExecTime > delay) {
|
||||
|
||||
@@ -343,7 +343,7 @@ const getConnectionStatusType = (state: number) => {
|
||||
|
||||
// 数据处理
|
||||
const originalItems = computed(() => {
|
||||
const result: any[] = []
|
||||
const result:unknown[] = []
|
||||
|
||||
if (!ap || !Array.isArray(ap) || ap.length === 0) {
|
||||
return result
|
||||
@@ -384,8 +384,8 @@ const originalItems = computed(() => {
|
||||
|
||||
// 显示数据
|
||||
const displayItems = computed(() => {
|
||||
const result: any[] = []
|
||||
const processGroups: { [key: number]: any[] } = {}
|
||||
const result:unknown[] = []
|
||||
const processGroups: { [key: number]:unknown[] } = {}
|
||||
|
||||
originalItems.value.forEach((item) => {
|
||||
const processIndex = item.processIndex
|
||||
@@ -450,7 +450,7 @@ const shouldShowSticky = computed(() => {
|
||||
return currentIndex > stickyIndex
|
||||
})
|
||||
|
||||
const shouldHideItem = (item: any, index: number) => {
|
||||
const shouldHideItem = (itemunknown, index: number) => {
|
||||
if (!stickyItem.value || !shouldShowSticky.value) return false
|
||||
return item.key === stickyItem.value.key
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ const formatBytes = (bytes: number) => {
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
const formatEndpoint = (endpoint: any) => {
|
||||
const formatEndpoint = (endpoint:unknown) => {
|
||||
return `${endpoint.address}:${endpoint.port}`
|
||||
}
|
||||
|
||||
@@ -434,8 +434,8 @@ const getConnectionStateType = (state: number, isActive: boolean) => {
|
||||
return types[state as keyof typeof types] || 'default'
|
||||
}
|
||||
|
||||
const getProcessStatus = (process: any) => {
|
||||
const hasActiveConnections = process.connections?.some((c: any) => c.isActive)
|
||||
const getProcessStatus = (processunknown) => {
|
||||
const hasActiveConnections = process.connections?.some((cunknown) => c.isActive)
|
||||
if (hasActiveConnections) {
|
||||
return { type: 'success', text: '活跃', show: true }
|
||||
}
|
||||
@@ -444,7 +444,7 @@ const getProcessStatus = (process: any) => {
|
||||
|
||||
// 数据处理
|
||||
const originalItems = computed(() => {
|
||||
const result: any[] = []
|
||||
const result:unknown[] = []
|
||||
|
||||
if (!ap || !Array.isArray(ap) || ap.length === 0) {
|
||||
return result
|
||||
@@ -486,8 +486,8 @@ const originalItems = computed(() => {
|
||||
|
||||
// 显示数据
|
||||
const displayItems = computed(() => {
|
||||
const result: any[] = []
|
||||
const processGroups: { [key: number]: any[] } = {}
|
||||
const result:unknown[] = []
|
||||
const processGroups: { [key: number]:unknown[] } = {}
|
||||
|
||||
originalItems.value.forEach((item) => {
|
||||
const processIndex = item.processIndex
|
||||
@@ -552,7 +552,7 @@ const shouldShowSticky = computed(() => {
|
||||
return currentIndex > stickyIndex
|
||||
})
|
||||
|
||||
const shouldHideItem = (item: any, index: number) => {
|
||||
const shouldHideItem = (item:unknown, index: number) => {
|
||||
if (!stickyItem.value || !shouldShowSticky.value) return false
|
||||
return item.key === stickyItem.value.key
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user