Skip to content

Commit 431fa13

Browse files
feat: 添加一键防护页面,支持实时状态监控
1 parent d37ca24 commit 431fa13

1 file changed

Lines changed: 310 additions & 0 deletions

File tree

templates/protection.html

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
<!DOCTYPE html>
2+
<html lang="zh-CN">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>一键保护 - CyberShield</title>
7+
<link rel="preconnect" href="https://fonts.googleapis.com">
8+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
9+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
10+
<style>
11+
:root {
12+
--bg: #0a0a0f;
13+
--bg-card: rgba(20, 20, 30, 0.6);
14+
--border: rgba(255,255,255,0.08);
15+
--text: #ffffff;
16+
--text-secondary: #8b8b9e;
17+
--accent: #6366f1;
18+
--accent-glow: rgba(99, 102, 241, 0.4);
19+
--success: #22c55e;
20+
--danger: #ef4444;
21+
}
22+
* { margin: 0; padding: 0; box-sizing: border-box; }
23+
body {
24+
font-family: 'Inter', -apple-system, sans-serif;
25+
background: var(--bg);
26+
color: var(--text);
27+
min-height: 100vh;
28+
overflow-x: hidden;
29+
}
30+
.cyber-bg { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -2; background: #000; }
31+
.cyber-bg canvas { width: 100%; height: 100%; }
32+
.bg-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.7) 100%); pointer-events: none; }
33+
34+
.navbar { position: fixed; top: 0; left: 0; right: 0; height: 64px; background: rgba(10, 10, 15, 0.9); backdrop-filter: blur(20px); border-bottom: 1px solid var(--border); z-index: 1000; display: flex; align-items: center; justify-content: center; }
35+
.navbar-inner { width: 100%; max-width: 1200px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; }
36+
.logo { display: flex; align-items: center; gap: 12px; font-weight: 700; font-size: 1.25rem; color: var(--text); text-decoration: none; }
37+
.logo-icon { width: 36px; height: 36px; background: linear-gradient(135deg, #6366f1, #8b5cf6); border-radius: 10px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px var(--accent-glow); }
38+
.nav-links { display: flex; align-items: center; gap: 8px; }
39+
.nav-link { padding: 10px 16px; color: var(--text-secondary); text-decoration: none; font-size: 0.9rem; font-weight: 500; border-radius: 8px; transition: all 0.2s; }
40+
.nav-link:hover { color: var(--text); background: rgba(255,255,255,0.05); }
41+
.nav-link.active { color: var(--text); background: rgba(255,255,255,0.08); }
42+
43+
.protection-section {
44+
padding: 100px 24px 60px;
45+
display: flex;
46+
flex-direction: column;
47+
align-items: center;
48+
justify-content: center;
49+
min-height: 100vh;
50+
}
51+
.protection-container { text-align: center; max-width: 500px; }
52+
.protection-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 40px; color: var(--text-secondary); }
53+
54+
.power-button-wrapper { position: relative; width: 220px; height: 220px; margin: 0 auto 40px; }
55+
.power-button-ring {
56+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
57+
border-radius: 50%; border: 3px solid rgba(255,255,255,0.1); transition: all 0.5s ease;
58+
}
59+
.power-button-ring.active { border-color: var(--success); box-shadow: 0 0 40px rgba(34, 197, 94, 0.4), inset 0 0 40px rgba(34, 197, 94, 0.1); }
60+
.power-button-ring.starting { border-color: #f59e0b; animation: pulseRing 1s ease-in-out infinite; }
61+
@keyframes pulseRing { 0%, 100% { box-shadow: 0 0 20px rgba(245, 158, 11, 0.3); } 50% { box-shadow: 0 0 50px rgba(245, 158, 11, 0.6); } }
62+
63+
.power-button {
64+
position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px;
65+
border-radius: 50%; background: linear-gradient(145deg, #1a1a2e, #0f0f1a); border: none; cursor: pointer;
66+
display: flex; flex-direction: column; align-items: center; justify-content: center; transition: all 0.3s ease;
67+
box-shadow: 0 10px 40px rgba(0,0,0,0.5), inset 0 -5px 20px rgba(0,0,0,0.3);
68+
}
69+
.power-button:hover { transform: scale(1.02); }
70+
.power-button:active { transform: scale(0.98); }
71+
.power-button .power-icon { font-size: 48px; color: rgba(255,255,255,0.3); transition: all 0.5s ease; margin-bottom: 8px; }
72+
.power-button.active .power-icon { color: var(--success); filter: drop-shadow(0 0 20px rgba(34, 197, 94, 0.8)); }
73+
.power-button.starting .power-icon { color: #f59e0b; animation: iconPulse 1s ease-in-out infinite; }
74+
@keyframes iconPulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
75+
.power-button .power-text { font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.4); text-transform: uppercase; letter-spacing: 2px; }
76+
.power-button.active .power-text { color: var(--success); }
77+
78+
.protection-status { font-size: 1.8rem; font-weight: 700; margin-bottom: 12px; transition: all 0.3s; }
79+
.protection-status.off { color: var(--text-secondary); }
80+
.protection-status.on { color: var(--success); text-shadow: 0 0 30px rgba(34, 197, 94, 0.5); }
81+
.protection-status.starting { color: #f59e0b; }
82+
.protection-desc { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 30px; }
83+
84+
.level-selector { display: flex; gap: 10px; justify-content: center; margin-bottom: 30px; flex-wrap: wrap; }
85+
.level-btn {
86+
padding: 10px 20px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
87+
border-radius: 25px; color: var(--text-secondary); font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.3s;
88+
}
89+
.level-btn:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.2); }
90+
.level-btn.active { background: linear-gradient(135deg, #6366f1, #8b5cf6); border-color: transparent; color: white; box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); }
91+
92+
.live-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-top: 20px; opacity: 0; transform: translateY(20px); transition: all 0.5s ease; }
93+
.live-stats.visible { opacity: 1; transform: translateY(0); }
94+
.stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; text-align: center; }
95+
.stat-card .stat-value { font-size: 1.5rem; font-weight: 700; background: linear-gradient(135deg, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
96+
.stat-card .stat-label { font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px; }
97+
.stat-card.danger .stat-value { background: linear-gradient(135deg, #ef4444, #f97316); -webkit-background-clip: text; }
98+
.stat-card.success .stat-value { background: linear-gradient(135deg, #22c55e, #10b981); -webkit-background-clip: text; }
99+
100+
@media (max-width: 768px) {
101+
.nav-links { display: none; }
102+
.live-stats { grid-template-columns: repeat(2, 1fr); }
103+
}
104+
</style>
105+
</head>
106+
<body>
107+
<div class="cyber-bg"><canvas id="cyberMapBg"></canvas></div>
108+
<div class="bg-overlay"></div>
109+
110+
<nav class="navbar">
111+
<div class="navbar-inner">
112+
<a href="/" class="logo"><div class="logo-icon"><i class="fas fa-shield-halved"></i></div><span>CyberShield</span></a>
113+
<div class="nav-links">
114+
<a href="/" class="nav-link">首页</a>
115+
<a href="/protection" class="nav-link active">一键保护</a>
116+
<a href="/dashboard" class="nav-link">仪表盘</a>
117+
<a href="/predict" class="nav-link">威胁检测</a>
118+
<a href="/train" class="nav-link">模型训练</a>
119+
<a href="/docs" class="nav-link" target="_blank">API</a>
120+
</div>
121+
</div>
122+
</nav>
123+
124+
<section class="protection-section">
125+
<div class="protection-container">
126+
<div class="protection-title">网络安全防护</div>
127+
<div class="power-button-wrapper">
128+
<div class="power-button-ring" id="powerRing"></div>
129+
<button class="power-button" id="powerButton" onclick="toggleProtection()">
130+
<i class="fas fa-power-off power-icon"></i>
131+
<span class="power-text" id="powerText">启动</span>
132+
</button>
133+
</div>
134+
<div class="protection-status off" id="protectionStatus">保护未开启</div>
135+
<div class="protection-desc" id="protectionDesc">点击上方按钮一键开启服务器防护</div>
136+
<div class="level-selector">
137+
<button class="level-btn" data-level="low">低级监控</button>
138+
<button class="level-btn active" data-level="medium">中级防护</button>
139+
<button class="level-btn" data-level="high">高级防护</button>
140+
<button class="level-btn" data-level="strict">严格模式</button>
141+
</div>
142+
<div class="live-stats" id="liveStats">
143+
<div class="stat-card"><div class="stat-value" id="statRequests">0</div><div class="stat-label">总请求</div></div>
144+
<div class="stat-card danger"><div class="stat-value" id="statBlocked">0</div><div class="stat-label">已拦截</div></div>
145+
<div class="stat-card"><div class="stat-value" id="statThreats">0</div><div class="stat-label">威胁数</div></div>
146+
<div class="stat-card success"><div class="stat-value" id="statUptime">0s</div><div class="stat-label">运行时间</div></div>
147+
</div>
148+
</div>
149+
</section>
150+
151+
<script>
152+
let protectionState = { status: 'off', level: 'medium' };
153+
let statsInterval = null;
154+
155+
async function toggleProtection() {
156+
const btn = document.getElementById('powerButton');
157+
const ring = document.getElementById('powerRing');
158+
const status = document.getElementById('protectionStatus');
159+
const desc = document.getElementById('protectionDesc');
160+
const stats = document.getElementById('liveStats');
161+
162+
if (protectionState.status === 'off') {
163+
btn.classList.add('starting'); ring.classList.add('starting');
164+
status.className = 'protection-status starting';
165+
status.textContent = '正在启动...';
166+
desc.textContent = '正在加载检测模型和初始化防护服务';
167+
document.getElementById('powerText').textContent = '启动中';
168+
169+
try {
170+
const res = await fetch('/api/v1/protection/start', {
171+
method: 'POST', headers: {'Content-Type': 'application/json'},
172+
body: JSON.stringify({level: protectionState.level})
173+
});
174+
const data = await res.json();
175+
if (data.success) {
176+
protectionState.status = 'on';
177+
updateUI(true, data.message);
178+
startStatsUpdate();
179+
} else { throw new Error(data.message); }
180+
} catch(e) {
181+
updateUI(false, e.message || '启动失败,请重试');
182+
}
183+
} else {
184+
try {
185+
await fetch('/api/v1/protection/stop', {method: 'POST'});
186+
protectionState.status = 'off';
187+
updateUI(false, '点击上方按钮一键开启服务器防护');
188+
stopStatsUpdate();
189+
} catch(e) { console.error('Stop failed:', e); }
190+
}
191+
}
192+
193+
function updateUI(isActive, message) {
194+
const btn = document.getElementById('powerButton');
195+
const ring = document.getElementById('powerRing');
196+
const status = document.getElementById('protectionStatus');
197+
const desc = document.getElementById('protectionDesc');
198+
const stats = document.getElementById('liveStats');
199+
200+
btn.classList.remove('starting', 'active');
201+
ring.classList.remove('starting', 'active');
202+
203+
if (isActive) {
204+
btn.classList.add('active'); ring.classList.add('active');
205+
status.className = 'protection-status on';
206+
status.textContent = '保护已开启';
207+
document.getElementById('powerText').textContent = '运行中';
208+
stats.classList.add('visible');
209+
} else {
210+
status.className = 'protection-status off';
211+
status.textContent = '保护未开启';
212+
document.getElementById('powerText').textContent = '启动';
213+
stats.classList.remove('visible');
214+
}
215+
desc.textContent = message;
216+
}
217+
218+
document.querySelectorAll('.level-btn').forEach(btn => {
219+
btn.addEventListener('click', async function() {
220+
document.querySelectorAll('.level-btn').forEach(b => b.classList.remove('active'));
221+
this.classList.add('active');
222+
protectionState.level = this.dataset.level;
223+
if (protectionState.status === 'on') {
224+
await fetch('/api/v1/protection/level', {
225+
method: 'POST', headers: {'Content-Type': 'application/json'},
226+
body: JSON.stringify({level: protectionState.level})
227+
});
228+
}
229+
});
230+
});
231+
232+
function startStatsUpdate() { updateStats(); statsInterval = setInterval(updateStats, 2000); }
233+
function stopStatsUpdate() { if (statsInterval) { clearInterval(statsInterval); statsInterval = null; } }
234+
235+
async function updateStats() {
236+
try {
237+
// 从stats API获取真实数据
238+
const [stateRes, statsRes] = await Promise.all([
239+
fetch('/api/v1/protection/state'),
240+
fetch('/api/v1/stats/overview?hours=24')
241+
]);
242+
const stateData = await stateRes.json();
243+
const statsData = await statsRes.json();
244+
245+
// 更新统计数据
246+
document.getElementById('statRequests').textContent = (statsData.total_requests || 0).toLocaleString();
247+
document.getElementById('statBlocked').textContent = (statsData.blocked_requests || 0).toLocaleString();
248+
document.getElementById('statThreats').textContent = (
249+
(statsData.threat_counts ? Object.values(statsData.threat_counts).reduce((a,b) => a + (b||0), 0) : 0) -
250+
(statsData.threat_counts?.benign || 0)
251+
).toLocaleString();
252+
253+
if (stateData.stats && stateData.stats.uptime_seconds) {
254+
const uptime = Math.floor(stateData.stats.uptime_seconds);
255+
if (uptime < 60) document.getElementById('statUptime').textContent = uptime + 's';
256+
else if (uptime < 3600) document.getElementById('statUptime').textContent = Math.floor(uptime/60) + 'm';
257+
else document.getElementById('statUptime').textContent = Math.floor(uptime/3600) + 'h';
258+
}
259+
} catch(e) { console.error('Stats update error:', e); }
260+
}
261+
262+
async function initState() {
263+
try {
264+
const res = await fetch('/api/v1/protection/state');
265+
const data = await res.json();
266+
protectionState.status = data.is_active ? 'on' : 'off';
267+
protectionState.level = data.level || 'medium';
268+
269+
document.querySelectorAll('.level-btn').forEach(b => {
270+
b.classList.toggle('active', b.dataset.level === protectionState.level);
271+
});
272+
273+
if (data.is_active) {
274+
updateUI(true, data.level_description || '防护运行中');
275+
startStatsUpdate();
276+
}
277+
} catch(e) { console.error('Init error:', e); }
278+
}
279+
initState();
280+
281+
// 背景动画
282+
const mapCanvas = document.getElementById('cyberMapBg');
283+
const ctx = mapCanvas.getContext('2d');
284+
let attacks = [];
285+
const nodes = [{x:0.12,y:0.38},{x:0.20,y:0.35},{x:0.47,y:0.30},{x:0.55,y:0.42},{x:0.77,y:0.35},{x:0.88,y:0.72}];
286+
const colors = ['#ef4444','#f59e0b','#22c55e','#3b82f6','#a855f7'];
287+
function resizeCanvas() { mapCanvas.width = window.innerWidth; mapCanvas.height = window.innerHeight; }
288+
function genAttack() {
289+
const s = nodes[Math.floor(Math.random()*nodes.length)];
290+
let d = nodes[Math.floor(Math.random()*nodes.length)];
291+
while(d===s) d = nodes[Math.floor(Math.random()*nodes.length)];
292+
attacks.push({s,d,p:0,c:colors[Math.floor(Math.random()*colors.length)],sp:0.01+Math.random()*0.015});
293+
if(attacks.length>12) attacks.shift();
294+
}
295+
function draw() {
296+
const w=mapCanvas.width, h=mapCanvas.height;
297+
ctx.fillStyle='rgba(0,0,0,0.15)'; ctx.fillRect(0,0,w,h);
298+
nodes.forEach(n=>{ctx.beginPath();ctx.arc(n.x*w,n.y*h,3,0,Math.PI*2);ctx.fillStyle='rgba(99,102,241,0.5)';ctx.fill();});
299+
attacks.forEach((a,i)=>{
300+
a.p+=a.sp; if(a.p>=1){attacks.splice(i,1);return;}
301+
const sx=a.s.x*w,sy=a.s.y*h,dx=a.d.x*w,dy=a.d.y*h,cx=(sx+dx)/2,cy=Math.min(sy,dy)-60,t=a.p;
302+
const px=(1-t)*(1-t)*sx+2*(1-t)*t*cx+t*t*dx, py=(1-t)*(1-t)*sy+2*(1-t)*t*cy+t*t*dy;
303+
ctx.beginPath();ctx.arc(px,py,3,0,Math.PI*2);ctx.fillStyle=a.c;ctx.fill();
304+
});
305+
requestAnimationFrame(draw);
306+
}
307+
resizeCanvas(); window.addEventListener('resize',resizeCanvas); setInterval(genAttack,600); draw();
308+
</script>
309+
</body>
310+
</html>

0 commit comments

Comments
 (0)