U:调整合并所有颜色样式;拆分MonitorView组件

This commit is contained in:
jimmy committed 2025-06-24 11:16:04 +08:00
1 parent c374165dcb
commit c9a876d5da
13 files changed
+2348 -262

No files matched your search

+88 -57
View File
@@ -1,71 +1,30 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
/* base.css - 基础样式和CSS重置 */
/* ==================== CSS Reset ==================== */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
font-weight: normal;
}
/* ==================== 根元素样式 ==================== */
html {
line-height: 1.6;
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
tab-size: 4;
}
/* ==================== Body 基础样式 ==================== */
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family:
Inter,
-apple-system,
@@ -80,7 +39,79 @@ body {
'Helvetica Neue',
sans-serif;
font-size: 15px;
line-height: 1.6;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: var(--transition);
}
/* 防止页面初始加载时的闪烁 */
body:not(.theme-ready) * {
transition: none !important;
}
/* ==================== 主题切换过渡效果 ==================== */
* {
/* 确保所有元素都使用主题过渡 */
transition: background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
}
/* ==================== App 容器 ==================== */
#app {
min-height: 100vh;
background-color: var(--bg-primary);
}
/* ==================== 排版基础 ==================== */
h1, h2, h3, h4, h5, h6 {
color: var(--text-primary);
font-weight: 600;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0.5rem;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }
h4 { font-size: 1.5rem; }
h5 { font-size: 1.25rem; }
h6 { font-size: 1rem; }
p {
margin-top: 0;
margin-bottom: 1rem;
}
/* ==================== 链接样式 ==================== */
a {
color: var(--accent-primary);
text-decoration: none;
transition: var(--transition);
}
a:hover {
color: var(--accent-secondary);
text-decoration: underline;
}
/* ==================== 列表样式 ==================== */
ul, ol {
margin-top: 0;
margin-bottom: 1rem;
padding-left: 1.5rem;
}
li {
list-style: none;
}
/* 恢复有序和无序列表的样式 */
ul.list-disc li { list-style: disc; }
ul.list-circle li { list-style: circle; }
ol.list-decimal li { list-style: decimal; }
/* ==================== 表单元素基础样式 ==================== */
+71 -192
View File
@@ -1,94 +1,15 @@
/* main.css - 主样式文件 */
/* ==================== 导入基础文件 ==================== */
@import './theme-variables.css';
@import './base.css';
/* 在你的全局 CSS 文件中 */
:root {
--bg-primary: #ffffff;
--text-primary: #000000;
transition: background-color 0.2s ease, color 0.2s ease;
}
:root.dark {
--bg-primary: #0c0c0c;
--text-primary: #ffffff;
}
html, body {
background-color: var(--bg-primary);
color: var(--text-primary);
margin: 0;
padding: 0;
transition: background-color 0.2s ease, color 0.2s ease;
}
#app {
min-height: 100vh;
background-color: var(--bg-primary);
}
li{
list-style: none;
}
/* ==================== 全局滚动条样式 ==================== */
/* ==================== 全局滚动条样式 ==================== */
/* 放置在 src/styles/scrollbar.css 或 src/assets/styles/scrollbar.css */
/* 基础滚动条样式 */
/* 基础滚动条配置 */
* {
/* Firefox 滚动条样式 */
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) rgba(255, 255, 255, 0.05);
}
/* Webkit 滚动条整体 */
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
/* 滚动条轨道 */
*::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.02);
border-radius: 5px;
margin: 2px;
}
/* 滚动条滑块 - 增强黑暗模式下的可见性 */
*::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: inset 0 0 2px rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
/* 滚动条滑块悬停效果 - 更明显的反馈 */
*::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.2);
box-shadow: inset 0 0 4px rgba(255, 255, 255, 0.2);
}
/* 滚动条滑块激活效果 */
*::-webkit-scrollbar-thumb:active {
background: rgba(255, 255, 255, 0.35);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.3);
}
/* 滚动条角落 */
*::-webkit-scrollbar-corner {
background: transparent;
}
/* ==================== 主题色滚动条样式 ==================== */
/* ==================== 全局滚动条样式 ==================== */
/* 基础滚动条配置 */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.25) rgba(255, 255, 255, 0.05);
scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-track-bg);
}
/* Webkit 滚动条整体配置 */
@@ -99,7 +20,7 @@ li{
/* 滚动条轨道 */
*::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
background: var(--scrollbar-track-bg);
border-radius: 4px;
margin: 2px;
}
@@ -108,13 +29,13 @@ li{
*::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.25) 100%
var(--scrollbar-thumb-bg) 0%,
color-mix(in srgb, var(--scrollbar-thumb-bg) 90%, transparent) 50%,
color-mix(in srgb, var(--scrollbar-thumb-bg) 110%, black) 100%
);
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--scrollbar-thumb-border);
transition: var(--transition-fast);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 1px 2px rgba(0, 0, 0, 0.1);
@@ -124,11 +45,11 @@ li{
*::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.35) 0%,
rgba(255, 255, 255, 0.25) 50%,
rgba(255, 255, 255, 0.4) 100%
var(--scrollbar-thumb-bg-hover) 0%,
color-mix(in srgb, var(--scrollbar-thumb-bg-hover) 85%, transparent) 50%,
color-mix(in srgb, var(--scrollbar-thumb-bg-hover) 115%, black) 100%
);
border-color: rgba(255, 255, 255, 0.2);
border-color: var(--scrollbar-thumb-border-hover);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 2px 4px rgba(0, 0, 0, 0.15);
@@ -139,107 +60,105 @@ li{
*::-webkit-scrollbar-thumb:active {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.45) 0%,
rgba(255, 255, 255, 0.35) 50%,
rgba(255, 255, 255, 0.5) 100%
var(--scrollbar-thumb-bg-active) 0%,
color-mix(in srgb, var(--scrollbar-thumb-bg-active) 80%, transparent) 50%,
color-mix(in srgb, var(--scrollbar-thumb-bg-active) 120%, black) 100%
);
border-color: rgba(255, 255, 255, 0.3);
border-color: var(--scrollbar-thumb-border-active);
transform: scaleY(1.1);
}
/* 滚动条角落 */
*::-webkit-scrollbar-corner {
background: rgba(255, 255, 255, 0.03);
background: var(--scrollbar-corner-bg);
}
/* ==================== 主题化滚动条变体 ==================== */
/* 主题色滚动条 - 蓝色系 */
.scrollbar-primary::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(99, 102, 241, 0.3) 0%,
rgba(99, 102, 241, 0.2) 50%,
rgba(99, 102, 241, 0.35) 100%
color-mix(in srgb, var(--accent-primary) 30%, transparent) 0%,
color-mix(in srgb, var(--accent-primary) 20%, transparent) 50%,
color-mix(in srgb, var(--accent-primary) 35%, transparent) 100%
);
border-color: rgba(99, 102, 241, 0.2);
border-color: color-mix(in srgb, var(--accent-primary) 20%, transparent);
}
.scrollbar-primary::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
rgba(99, 102, 241, 0.5) 0%,
rgba(99, 102, 241, 0.35) 50%,
rgba(99, 102, 241, 0.55) 100%
color-mix(in srgb, var(--accent-primary) 50%, transparent) 0%,
color-mix(in srgb, var(--accent-primary) 35%, transparent) 50%,
color-mix(in srgb, var(--accent-primary) 55%, transparent) 100%
);
border-color: rgba(99, 102, 241, 0.3);
border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
}
/* 成功色滚动条 - 绿色系 */
.scrollbar-success::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(34, 197, 94, 0.3) 0%,
rgba(34, 197, 94, 0.2) 50%,
rgba(34, 197, 94, 0.35) 100%
color-mix(in srgb, var(--accent-success) 30%, transparent) 0%,
color-mix(in srgb, var(--accent-success) 20%, transparent) 50%,
color-mix(in srgb, var(--accent-success) 35%, transparent) 100%
);
border-color: rgba(34, 197, 94, 0.2);
border-color: color-mix(in srgb, var(--accent-success) 20%, transparent);
}
.scrollbar-success::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
rgba(34, 197, 94, 0.5) 0%,
rgba(34, 197, 94, 0.35) 50%,
rgba(34, 197, 94, 0.55) 100%
color-mix(in srgb, var(--accent-success) 50%, transparent) 0%,
color-mix(in srgb, var(--accent-success) 35%, transparent) 50%,
color-mix(in srgb, var(--accent-success) 55%, transparent) 100%
);
border-color: rgba(34, 197, 94, 0.3);
border-color: color-mix(in srgb, var(--accent-success) 30%, transparent);
}
/* 警告色滚动条 - 橙色系 */
.scrollbar-warning::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(245, 158, 11, 0.3) 0%,
rgba(245, 158, 11, 0.2) 50%,
rgba(245, 158, 11, 0.35) 100%
color-mix(in srgb, var(--accent-warning) 30%, transparent) 0%,
color-mix(in srgb, var(--accent-warning) 20%, transparent) 50%,
color-mix(in srgb, var(--accent-warning) 35%, transparent) 100%
);
border-color: rgba(245, 158, 11, 0.2);
border-color: color-mix(in srgb, var(--accent-warning) 20%, transparent);
}
.scrollbar-warning::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
rgba(245, 158, 11, 0.5) 0%,
rgba(245, 158, 11, 0.35) 50%,
rgba(245, 158, 11, 0.55) 100%
color-mix(in srgb, var(--accent-warning) 50%, transparent) 0%,
color-mix(in srgb, var(--accent-warning) 35%, transparent) 50%,
color-mix(in srgb, var(--accent-warning) 55%, transparent) 100%
);
border-color: rgba(245, 158, 11, 0.3);
border-color: color-mix(in srgb, var(--accent-warning) 30%, transparent);
}
/* 紫色滚动条 */
.scrollbar-purple::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(168, 85, 247, 0.3) 0%,
rgba(168, 85, 247, 0.2) 50%,
rgba(168, 85, 247, 0.35) 100%
color-mix(in srgb, var(--accent-purple) 30%, transparent) 0%,
color-mix(in srgb, var(--accent-purple) 20%, transparent) 50%,
color-mix(in srgb, var(--accent-purple) 35%, transparent) 100%
);
border-color: rgba(168, 85, 247, 0.2);
border-color: color-mix(in srgb, var(--accent-purple) 20%, transparent);
}
.scrollbar-purple::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
rgba(168, 85, 247, 0.5) 0%,
rgba(168, 85, 247, 0.35) 50%,
rgba(168, 85, 247, 0.55) 100%
color-mix(in srgb, var(--accent-purple) 50%, transparent) 0%,
color-mix(in srgb, var(--accent-purple) 35%, transparent) 50%,
color-mix(in srgb, var(--accent-purple) 55%, transparent) 100%
);
border-color: rgba(168, 85, 247, 0.3);
border-color: color-mix(in srgb, var(--accent-purple) 30%, transparent);
}
/* ==================== 尺寸变体 ==================== */
/* ==================== 滚动条尺寸变体 ==================== */
/* 细滚动条 */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
@@ -252,56 +171,7 @@ li{
height: 12px;
}
/* ==================== 浅色模式适配 ==================== */
@media (prefers-color-scheme: light) {
* {
scrollbar-color: rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.08);
}
*::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.04);
}
*::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.15) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.2) 100%
);
border-color: rgba(0, 0, 0, 0.1);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
0 1px 2px rgba(0, 0, 0, 0.1);
}
*::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.25) 0%,
rgba(0, 0, 0, 0.2) 50%,
rgba(0, 0, 0, 0.3) 100%
);
border-color: rgba(0, 0, 0, 0.15);
}
*::-webkit-scrollbar-thumb:active {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.35) 0%,
rgba(0, 0, 0, 0.3) 50%,
rgba(0, 0, 0, 0.4) 100%
);
border-color: rgba(0, 0, 0, 0.2);
}
*::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0.04);
}
}
/* ==================== 特殊状态 ==================== */
/* ==================== 滚动条特殊状态 ==================== */
/* 禁用状态 */
.scrollbar-disabled::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.05) !important;
@@ -319,8 +189,7 @@ li{
display: none;
}
/* ==================== 动画增强 ==================== */
/* ==================== 滚动条动画增强 ==================== */
/* 滚动时的轨道高亮效果 */
@keyframes scrollTrackPulse {
0%, 100% {
@@ -354,13 +223,12 @@ li{
animation: thumbBreathe 2s ease-in-out infinite;
}
/* ==================== 使用说明(CSS 注释) ==================== */
/* ==================== 滚动条使用说明 ==================== */
/*
使用方法:
滚动条样式使用方法:
1. 基础用法:
直接引入此 CSS 文件,所有元素将自动应用默认滚动条样式
所有元素默认应用全局滚动条样式
2. 主题化用法:
<div class="scrollbar-primary">...</div> // 蓝色主题
@@ -382,5 +250,16 @@ li{
6. 隐藏滚动条:
<div class="scrollbar-hidden">...</div>
注意:此样式自动适配浅色/深色模式
7. 自定义颜色:
可以通过修改 theme-variables.css 中的滚动条变量来自定义颜色:
- --scrollbar-track-bg: 轨道背景色
- --scrollbar-thumb-bg: 滑块背景色
- --scrollbar-thumb-bg-hover: 滑块悬停背景色
- --scrollbar-thumb-bg-active: 滑块激活背景色
- --scrollbar-thumb-border: 滑块边框色
- --scrollbar-thumb-border-hover: 滑块悬停边框色
- --scrollbar-thumb-border-active: 滑块激活边框色
- --scrollbar-corner-bg: 角落背景色
注意:滚动条样式自动适配浅色/深色模式
*/
@@ -2,8 +2,31 @@
/* ==================== 主题切换的根选择器 ==================== */
:root {
/* ========== 颜色定义 - 亮色主题(默认) ========== */
/* ========== 基础颜色定义 ========== */
/* Vue主题色板 */
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
/* 分割线颜色 */
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
/* 文本颜色 */
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
/* ========== 颜色定义 - 亮色主题(默认) ========== */
/* 背景色层级 */
--bg-primary: #ffffff; /* 主背景色 */
--bg-secondary: #f8fafc; /* 次级背景 */
@@ -47,6 +70,30 @@
/* 其他效果 */
--backdrop-blur: blur(20px);
--transition: all 0.2s ease;
--transition-fast: all 0.15s ease;
--transition-slow: all 0.3s ease;
/* 滚动条颜色 - 亮色主题 */
--scrollbar-track-bg: rgba(0, 0, 0, 0.04);
--scrollbar-thumb-bg: rgba(0, 0, 0, 0.15);
--scrollbar-thumb-bg-hover: rgba(0, 0, 0, 0.25);
--scrollbar-thumb-bg-active: rgba(0, 0, 0, 0.35);
--scrollbar-thumb-border: rgba(0, 0, 0, 0.1);
--scrollbar-thumb-border-hover: rgba(0, 0, 0, 0.15);
--scrollbar-thumb-border-active: rgba(0, 0, 0, 0.2);
--scrollbar-corner-bg: rgba(0, 0, 0, 0.04);
/* 语义化颜色变量(向后兼容)*/
--color-background: var(--bg-primary);
--color-background-soft: var(--bg-secondary);
--color-background-mute: var(--bg-tertiary);
--color-border: var(--border-primary);
--color-border-hover: var(--border-hover);
--color-heading: var(--text-primary);
--color-text: var(--text-secondary);
/* 布局 */
--section-gap: 160px;
}
/* ==================== 暗色主题 - .dark 类名方式 ==================== */
@@ -78,10 +125,29 @@
--border-focus: #3b82f6;
/* 阴影 - 暗色模式 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
/* 滚动条颜色 - 暗色主题 */
--scrollbar-track-bg: rgba(255, 255, 255, 0.03);
--scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
--scrollbar-thumb-bg-hover: rgba(255, 255, 255, 0.35);
--scrollbar-thumb-bg-active: rgba(255, 255, 255, 0.45);
--scrollbar-thumb-border: rgba(255, 255, 255, 0.1);
--scrollbar-thumb-border-hover: rgba(255, 255, 255, 0.2);
--scrollbar-thumb-border-active: rgba(255, 255, 255, 0.3);
--scrollbar-corner-bg: rgba(255, 255, 255, 0.03);
/* 语义化颜色变量(向后兼容)*/
--color-background: var(--bg-primary);
--color-background-soft: var(--bg-secondary);
--color-background-mute: var(--bg-tertiary);
--color-border: var(--border-primary);
--color-border-hover: var(--border-hover);
--color-heading: var(--text-primary);
--color-text: var(--text-secondary);
}
/* ==================== 暗色主题 - data-theme 属性方式(向后兼容) ==================== */
@@ -112,11 +178,86 @@
--border-hover: rgba(59, 130, 246, 0.5);
--border-focus: #3b82f6;
/* 阴影 - 暗色模式下的阴影更深 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* 阴影 - 暗色模式 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
/* 滚动条颜色 - 暗色主题 */
--scrollbar-track-bg: rgba(255, 255, 255, 0.03);
--scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
--scrollbar-thumb-bg-hover: rgba(255, 255, 255, 0.35);
--scrollbar-thumb-bg-active: rgba(255, 255, 255, 0.45);
--scrollbar-thumb-border: rgba(255, 255, 255, 0.1);
--scrollbar-thumb-border-hover: rgba(255, 255, 255, 0.2);
--scrollbar-thumb-border-active: rgba(255, 255, 255, 0.3);
--scrollbar-corner-bg: rgba(255, 255, 255, 0.03);
/* 语义化颜色变量(向后兼容)*/
--color-background: var(--bg-primary);
--color-background-soft: var(--bg-secondary);
--color-background-mute: var(--bg-tertiary);
--color-border: var(--border-primary);
--color-border-hover: var(--border-hover);
--color-heading: var(--text-primary);
--color-text: var(--text-secondary);
}
/* ==================== 媒体查询 - 系统主题偏好 ==================== */
@media (prefers-color-scheme: dark) {
:root:not(.light) {
/* 背景色层级 - 暗色 */
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-quaternary: #475569;
--bg-overlay: rgba(15, 23, 42, 0.95);
--bg-glass: rgba(30, 41, 59, 0.8);
--bg-card: rgba(51, 65, 85, 0.5);
--bg-hover: rgba(59, 130, 246, 0.1);
--bg-selected: rgba(59, 130, 246, 0.2);
/* 文字色层级 - 暗色 */
--text-primary: #f8fafc;
--text-secondary: #f1f5f9;
--text-tertiary: #e2e8f0;
--text-quaternary: #cbd5e1;
--text-muted: #94a3b8;
--text-disabled: #64748b;
/* 边框色层级 - 暗色 */
--border-primary: #334155;
--border-secondary: rgba(51, 65, 85, 0.3);
--border-tertiary: rgba(71, 85, 105, 0.3);
--border-hover: rgba(59, 130, 246, 0.5);
--border-focus: #3b82f6;
/* 阴影 - 暗色模式 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
/* 滚动条颜色 - 暗色主题 */
--scrollbar-track-bg: rgba(255, 255, 255, 0.03);
--scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
--scrollbar-thumb-bg-hover: rgba(255, 255, 255, 0.35);
--scrollbar-thumb-bg-active: rgba(255, 255, 255, 0.45);
--scrollbar-thumb-border: rgba(255, 255, 255, 0.1);
--scrollbar-thumb-border-hover: rgba(255, 255, 255, 0.2);
--scrollbar-thumb-border-active: rgba(255, 255, 255, 0.3);
--scrollbar-corner-bg: rgba(255, 255, 255, 0.03);
/* 语义化颜色变量(向后兼容)*/
--color-background: var(--bg-primary);
--color-background-soft: var(--bg-secondary);
--color-background-mute: var(--bg-tertiary);
--color-border: var(--border-primary);
--color-border-hover: var(--border-hover);
--color-heading: var(--text-primary);
--color-text: var(--text-secondary);
}
}
/* ==================== 主题切换过渡效果 ==================== */
-2
View File
@@ -1,5 +1,3 @@
// 引入主题变量(放在其他样式之前)
import './assets/theme-variables.css'
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
+1 -1
View File
@@ -12,7 +12,7 @@ import NotFound from '@/views/NotFoundView.vue'
import AboutView from '@/views/AboutView.vue'
import FirewallView from '@/views/FirewallView.vue'
import AnalyseView from '@/views/AnalyseView.vue'
import MonitorView from '@/views/MonitorView.vue'
import MonitorView from '@/views/MonitorView/index.vue'
// 导入所有 .vue 文件
const routes = import.meta.glob('@/views/dev/**/*.vue')
@@ -0,0 +1,390 @@
<template>
<div class="inspector scrollbar-purple scrollbar-glow" :style="{ width: width + 'px' }">
<div class="inspector-content">
<div class="inspector-header">
<h3 class="inspector-title">
<span class="title-icon">🔍</span>
连接详情
</h3>
</div>
<div v-if="selectedConnection" class="inspector-body">
<!-- 基本信息 -->
<div class="detail-section">
<h4 class="detail-section-title">基本信息</h4>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">进程</span>
<span class="detail-value">{{ selectedConnection.process }}</span>
</div>
<div class="detail-item">
<span class="detail-label">PID</span>
<span class="detail-value">{{ selectedConnection.pid || '12345' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">协议</span>
<span class="detail-value">{{ selectedConnection.protocol || 'TCP' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">状态</span>
<span class="detail-value" :class="`status-${selectedConnection.status}`">
{{ getStatusText(selectedConnection.status) }}
</span>
</div>
</div>
</div>
<!-- 网络信息 -->
<div class="detail-section">
<h4 class="detail-section-title">网络信息</h4>
<div class="network-info">
<div class="network-item">
<div class="network-label">本地地址</div>
<div class="network-value">{{ selectedConnection.localAddress }}:{{ selectedConnection.localPort || '54321' }}</div>
</div>
<div class="network-item">
<div class="network-label">远程地址</div>
<div class="network-value">{{ selectedConnection.remoteIp || '142.250.191.14' }}:{{ selectedConnection.remotePort || '443' }}</div>
</div>
<div class="network-item">
<div class="network-label">域名</div>
<div class="network-value">{{ selectedConnection.remoteAddress }}</div>
</div>
</div>
</div>
<!-- 流量统计 -->
<div class="detail-section">
<h4 class="detail-section-title">流量统计</h4>
<div class="traffic-stats">
<div class="traffic-item">
<span class="traffic-label">上传</span>
<span class="traffic-value upload">{{ trafficData.upload }}</span>
</div>
<div class="traffic-item">
<span class="traffic-label">下载</span>
<span class="traffic-value download">{{ trafficData.download }}</span>
</div>
<div class="traffic-item">
<span class="traffic-label">总计</span>
<span class="traffic-value total">{{ trafficData.total }}</span>
</div>
</div>
<TrafficChart :data="chartData" />
</div>
<!-- 安全信息 -->
<div class="detail-section">
<h4 class="detail-section-title">安全信息</h4>
<div class="security-info">
<div class="security-item safe">
<div class="security-icon">✅</div>
<div class="security-text">
<div class="security-title">连接安全</div>
<div class="security-desc">HTTPS 加密连接</div>
</div>
</div>
<div class="security-item safe">
<div class="security-icon">🛡️</div>
<div class="security-text">
<div class="security-title">证书有效</div>
<div class="security-desc">由 Google Trust Services 签发</div>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-icon">📊</div>
<div class="empty-text">选择一个连接以查看详情</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import TrafficChart from './components/TrafficChart.vue'
// Props
const props = defineProps<{
width: number
selectedConnection: any | null
}>()
// 状态文本映射
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
established: '已建立',
listening: '监听中',
close_wait: '等待关闭',
time_wait: '时间等待',
closed: '已关闭'
}
return statusMap[status] || status
}
// 流量数据
const trafficData = computed(() => {
if (!props.selectedConnection) {
return { upload: '0 B', download: '0 B', total: '0 B' }
}
// 模拟数据
return {
upload: '1.2 MB',
download: '5.8 MB',
total: '7.0 MB'
}
})
// 图表数据
const chartData = computed(() => {
// 生成模拟的流量图表数据
return Array.from({ length: 20 }, () => Math.random() * 100)
})
</script>
<style scoped>
/* 检查器容器 */
.inspector {
background: var(--bg-glass);
backdrop-filter: var(--backdrop-blur);
border-left: 1px solid var(--border-primary);
overflow: hidden;
flex-shrink: 0;
}
.inspector-content {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* 头部 */
.inspector-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-secondary);
background: var(--bg-card);
flex-shrink: 0;
}
.inspector-title {
font-size: 16px;
font-weight: 600;
color: var(--text-secondary);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.title-icon {
font-size: 16px;
}
/* 主体内容 */
.inspector-body {
flex: 1;
padding: 24px;
min-height: 0;
overflow-y: auto;
}
/* 详情区块 */
.detail-section {
margin-bottom: 24px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section-title {
font-size: 14px;
font-weight: 600;
color: var(--text-quaternary);
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-tertiary);
}
/* 基本信息网格 */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.detail-value {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.detail-value.status-established {
color: var(--accent-success);
}
/* 网络信息 */
.network-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.network-item {
padding: 12px;
background: var(--bg-card);
border-radius: 8px;
border: 1px solid var(--border-tertiary);
}
.network-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
margin-bottom: 4px;
}
.network-value {
font-size: 13px;
color: var(--text-secondary);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-weight: 500;
}
/* 流量统计 */
.traffic-stats {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.traffic-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: var(--bg-card);
border-radius: 6px;
}
.traffic-label {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.traffic-value {
font-size: 13px;
font-weight: 600;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.traffic-value.upload {
color: var(--accent-warning);
}
.traffic-value.download {
color: var(--accent-secondary);
}
.traffic-value.total {
color: var(--accent-purple);
}
/* 安全信息 */
.security-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.security-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
border: 1px solid var(--border-tertiary);
}
.security-item.safe {
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.2);
}
.security-icon {
font-size: 16px;
width: 24px;
text-align: center;
}
.security-text {
flex: 1;
}
.security-title {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
line-height: 1;
}
.security-desc {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--text-muted);
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 14px;
text-align: center;
}
/* 响应式 */
@media (max-width: 1200px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,219 @@
<template>
<div class="main-view">
<!-- 顶部 - 搜索和标题 -->
<div class="main-header" :style="{ height: headerHeight + 'px' }">
<div class="header-content">
<div class="header-info">
<h2 class="main-header-title">网络连接监控</h2>
<p class="main-header-subtitle">实时监控系统网络活动</p>
</div>
<div class="header-actions">
<div class="search-area">
<n-input
:value="searchQuery"
@update:value="$emit('update:searchQuery', $event)"
placeholder="搜索连接、域名或IP..."
size="medium"
round
>
<template #prefix>
<n-icon :component="SearchOutline" />
</template>
</n-input>
</div>
<div class="filter-buttons">
<n-button size="small" type="primary" ghost @click="handleFilter">过滤</n-button>
<n-button size="small" ghost @click="handleRefresh">刷新</n-button>
</div>
</div>
</div>
</div>
<!-- 连接列表 -->
<ConnectionsTable
:connections="filteredConnections"
:isMonitoring="isMonitoring"
@select="$emit('selectConnection', $event)"
@toggleMonitoring="isMonitoring = !isMonitoring"
/>
<!-- 时间轴分割线 -->
<div class="resize-handle-horizontal" @mousedown="$emit('resizeTimeline', $event)">
<div class="resize-handle-hover-h"></div>
</div>
<!-- 时间轴事件流 -->
<EventTimeline
:height="timelineHeight"
:events="events"
:isPaused="isTimelinePaused"
@pause="isTimelinePaused = !isTimelinePaused"
@clear="handleClearEvents"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { NButton, NInput, NIcon } from 'naive-ui'
import { SearchOutline } from '@vicons/ionicons5'
import ConnectionsTable from './components/ConnectionsTable.vue'
import EventTimeline from './components/EventTimeline.vue'
// Props
const props = defineProps<{
headerHeight: number
timelineHeight: number
searchQuery: string
connections: Array<any>
events: Array<any>
}>()
// Emits
const emit = defineEmits<{
'update:searchQuery': [value: string]
'resizeTimeline': [event: MouseEvent]
'selectConnection': [connection: any]
}>()
// Local state
const isMonitoring = ref(true)
const isTimelinePaused = ref(false)
// 过滤后的连接列表
const filteredConnections = computed(() => {
if (!props.searchQuery) return props.connections
const query = props.searchQuery.toLowerCase()
return props.connections.filter(conn =>
conn.process.toLowerCase().includes(query) ||
conn.localAddress.toLowerCase().includes(query) ||
conn.remoteAddress.toLowerCase().includes(query)
)
})
// 处理过滤
const handleFilter = () => {
console.log('打开过滤器')
}
// 处理刷新
const handleRefresh = () => {
console.log('刷新数据')
}
// 清空事件
const handleClearEvents = () => {
console.log('清空事件')
}
</script>
<style scoped>
/* 主视图容器 */
.main-view {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
background: var(--bg-card);
overflow: hidden;
}
/* 顶部区域 */
.main-header {
background: var(--bg-glass);
backdrop-filter: var(--backdrop-blur);
border-bottom: 1px solid var(--border-primary);
padding: 0 24px;
flex-shrink: 0;
}
.header-content {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-info {
flex: 1;
}
.main-header-title {
font-size: 20px;
font-weight: 700;
color: var(--text-secondary);
margin: 0;
line-height: 1;
}
.main-header-subtitle {
font-size: 14px;
color: var(--text-muted);
margin: 4px 0 0 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.search-area {
width: 300px;
}
.filter-buttons {
display: flex;
gap: 8px;
}
/* 时间轴分割线 */
.resize-handle-horizontal {
height: 4px;
background: var(--border-tertiary);
cursor: row-resize;
transition: background-color 0.2s;
position: relative;
z-index: 10;
flex-shrink: 0;
}
.resize-handle-horizontal:hover {
background: rgba(168, 85, 247, 0.5);
}
.resize-handle-hover-h {
position: absolute;
inset: -4px 0;
}
.resize-handle-horizontal:active {
background-color: var(--accent-primary) !important;
}
/* 自定义 Naive UI 样式 */
:deep(.n-input .n-input__input-el) {
background: var(--bg-card);
border-color: var(--border-tertiary);
color: var(--text-secondary);
}
:deep(.n-input:hover .n-input__input-el) {
border-color: var(--border-hover);
}
:deep(.n-input.n-input--focus .n-input__input-el) {
border-color: var(--accent-primary);
}
/* 响应式 */
@media (max-width: 1200px) {
.search-area {
width: 250px;
}
}
</style>
@@ -0,0 +1,251 @@
<template>
<div class="sidebar scrollbar-primary scrollbar-thin" :style="{ width: width + 'px' }">
<div class="sidebar-content">
<div class="sidebar-header">
<h3 class="sidebar-title">系统概览</h3>
<div class="sidebar-badge">{{ data.processCount }}</div>
</div>
<div class="sidebar-stats">
<div class="stat-card">
<div class="stat-icon">🔗</div>
<div class="stat-info">
<div class="stat-value">{{ data.activeConnections }}</div>
<div class="stat-label">活跃连接</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">⚡</div>
<div class="stat-info">
<div class="stat-value">{{ data.networkSpeed }}</div>
<div class="stat-label">网络速度</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🛡️</div>
<div class="stat-info">
<div class="stat-value">{{ data.ruleCount }}</div>
<div class="stat-label">防护规则</div>
</div>
</div>
</div>
<div class="sidebar-section">
<h4 class="section-title">快速操作</h4>
<div class="quick-actions">
<div class="action-item" @click="handleAction('scan')">扫描威胁</div>
<div class="action-item" @click="handleAction('update')">更新规则</div>
<div class="action-item" @click="handleAction('export')">导出日志</div>
</div>
</div>
<div class="sidebar-section">
<h4 class="section-title">系统状态</h4>
<div class="system-status">
<div class="status-item">
<span class="status-dot active"></span>
<span class="status-label">防护已启用</span>
</div>
<div class="status-item">
<span class="status-dot active"></span>
<span class="status-label">实时监控中</span>
</div>
<div class="status-item">
<span class="status-dot warning"></span>
<span class="status-label">待处理警告: 3</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
// Props
const props = defineProps<{
width: number
data: {
activeConnections: number
processCount: number
networkSpeed: string
ruleCount: number
}
}>()
// Emits
const emit = defineEmits<{
action: [type: string]
}>()
// 处理快速操作
const handleAction = (type: string) => {
emit('action', type)
console.log(`执行操作: ${type}`)
}
</script>
<style scoped>
/* 侧边栏容器 */
.sidebar {
background: var(--bg-glass);
backdrop-filter: var(--backdrop-blur);
border-right: 1px solid var(--border-primary);
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-content {
padding: 24px;
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* 头部 */
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.sidebar-title {
font-size: 16px;
font-weight: 600;
color: var(--text-secondary);
margin: 0;
}
.sidebar-badge {
background: linear-gradient(135deg, var(--accent-primary) 0%, #1d4ed8 100%);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
/* 统计卡片 */
.sidebar-stats {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border-tertiary);
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
transition: var(--transition);
}
.stat-card:hover {
background: var(--bg-hover);
border-color: var(--border-hover);
transform: translateY(-1px);
}
.stat-icon {
font-size: 20px;
width: 32px;
text-align: center;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--text-secondary);
line-height: 1;
}
.stat-label {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
/* 侧边栏区块 */
.sidebar-section {
margin-top: 24px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: var(--text-quaternary);
margin-bottom: 12px;
}
/* 快速操作 */
.quick-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.action-item {
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: 8px;
font-size: 13px;
color: var(--text-quaternary);
cursor: pointer;
transition: var(--transition);
}
.action-item:hover {
background: var(--bg-hover);
color: var(--text-secondary);
}
/* 系统状态 */
.system-status {
display: flex;
flex-direction: column;
gap: 10px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-tertiary);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-disabled);
}
.status-dot.active {
background: var(--accent-success);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
}
.status-dot.warning {
background: var(--accent-warning);
box-shadow: 0 0 0 2px rgba(251, 146, 60, 0.2);
}
.status-label {
flex: 1;
}
</style>
@@ -0,0 +1,383 @@
<template>
<div class="connections-area">
<div class="connections-header">
<h3 class="connections-title">
<span class="title-icon">📊</span>
连接列表
<span class="connection-count">{{ connections.length }} 个活跃连接</span>
</h3>
<div class="connections-controls">
<n-button
size="small"
:type="isMonitoring ? 'warning' : 'info'"
ghost
@click="$emit('toggleMonitoring')"
>
{{ isMonitoring ? '暂停' : '继续' }}监控
</n-button>
</div>
</div>
<div class="connections-content">
<div class="table-container">
<div class="table-header">
<div class="table-column" @click="handleSort('process')">
进程
<span v-if="sortColumn === 'process'" class="sort-icon">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</div>
<div class="table-column" @click="handleSort('localAddress')">
本地地址
<span v-if="sortColumn === 'localAddress'" class="sort-icon">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</div>
<div class="table-column" @click="handleSort('remoteAddress')">
远程地址
<span v-if="sortColumn === 'remoteAddress'" class="sort-icon">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</div>
<div class="table-column" @click="handleSort('status')">
状态
<span v-if="sortColumn === 'status'" class="sort-icon">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</div>
<div class="table-column" @click="handleSort('traffic')">
流量
<span v-if="sortColumn === 'traffic'" class="sort-icon">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</div>
</div>
<div class="table-body scrollbar-success scrollbar-glow">
<div
v-for="connection in sortedConnections"
:key="connection.id"
class="table-row"
:class="{ 'row-selected': selectedId === connection.id }"
@click="handleSelect(connection)"
>
<div class="table-cell">
<div class="process-info">
<div class="process-icon">{{ getProcessIcon(connection.process) }}</div>
<span>{{ connection.process }}</span>
</div>
</div>
<div class="table-cell">{{ connection.localAddress }}</div>
<div class="table-cell">{{ connection.remoteAddress }}</div>
<div class="table-cell">
<span class="status-badge" :class="`status-${connection.status}`">
{{ getStatusText(connection.status) }}
</span>
</div>
<div class="table-cell">
<span class="traffic-info">{{ connection.traffic }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-if="connections.length === 0" class="empty-table">
<div class="empty-icon">🔍</div>
<div class="empty-text">暂无连接数据</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { NButton } from 'naive-ui'
// Props
const props = defineProps<{
connections: Array<{
id: number
process: string
localAddress: string
remoteAddress: string
status: string
traffic: string
}>
isMonitoring: boolean
}>()
// Emits
const emit = defineEmits<{
select: [connection: any]
toggleMonitoring: []
}>()
// Local state
const selectedId = ref<number | null>(null)
const sortColumn = ref<string>('')
const sortOrder = ref<'asc' | 'desc'>('asc')
// 排序后的连接列表
const sortedConnections = computed(() => {
if (!sortColumn.value) return props.connections
return [...props.connections].sort((a, b) => {
const aVal = a[sortColumn.value as keyof typeof a]
const bVal = b[sortColumn.value as keyof typeof b]
if (sortOrder.value === 'asc') {
return aVal > bVal ? 1 : -1
} else {
return aVal < bVal ? 1 : -1
}
})
})
// 处理排序
const handleSort = (column: string) => {
if (sortColumn.value === column) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortColumn.value = column
sortOrder.value = 'asc'
}
}
// 处理选中
const handleSelect = (connection: any) => {
selectedId.value = connection.id
emit('select', connection)
}
// 获取进程图标
const getProcessIcon = (process: string) => {
const iconMap: Record<string, string> = {
'chrome.exe': '🌐',
'firefox.exe': '🦊',
'edge.exe': '🌊',
'node.exe': '💚',
'python.exe': '🐍',
default: '📦'
}
return iconMap[process.toLowerCase()] || iconMap.default
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
established: '已建立',
listening: '监听中',
close_wait: '等待关闭',
time_wait: '时间等待',
closed: '已关闭'
}
return statusMap[status] || status
}
</script>
<style scoped>
/* 连接区域容器 */
.connections-area {
flex: 1;
background: var(--bg-glass);
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 头部 */
.connections-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-secondary);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-card);
flex-shrink: 0;
}
.connections-title {
font-size: 16px;
font-weight: 600;
color: var(--text-secondary);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.title-icon {
font-size: 16px;
}
.connection-count {
font-size: 12px;
color: var(--text-muted);
font-weight: 400;
margin-left: 8px;
}
/* 内容区域 */
.connections-content {
flex: 1;
overflow: hidden;
padding: 0;
min-height: 0;
}
.table-container {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 表头 */
.table-header {
display: flex;
padding: 12px 24px;
background: var(--bg-card);
border-bottom: 1px solid var(--border-tertiary);
font-size: 12px;
font-weight: 600;
color: var(--text-quaternary);
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.table-column {
flex: 1;
padding: 0 8px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 4px;
transition: color 0.2s;
}
.table-column:hover {
color: var(--text-secondary);
}
.table-column:first-child {
flex: 1.5;
}
.sort-icon {
font-size: 10px;
opacity: 0.7;
}
/* 表体 */
.table-body {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.table-row {
display: flex;
padding: 12px 24px;
border-bottom: 1px solid var(--border-secondary);
transition: var(--transition);
cursor: pointer;
}
.table-row:hover {
background: var(--bg-hover);
}
.table-row.row-selected {
background: var(--bg-selected);
border-color: var(--border-hover);
}
.table-cell {
flex: 1;
padding: 0 8px;
display: flex;
align-items: center;
font-size: 13px;
color: var(--text-tertiary);
}
.table-cell:first-child {
flex: 1.5;
}
/* 进程信息 */
.process-info {
display: flex;
align-items: center;
gap: 8px;
}
.process-icon {
font-size: 14px;
}
/* 状态徽章 */
.status-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.status-established {
background: rgba(34, 197, 94, 0.2);
color: var(--accent-success);
border: 1px solid rgba(34, 197, 94, 0.3);
}
.status-badge.status-listening {
background: rgba(59, 130, 246, 0.2);
color: var(--accent-primary);
border: 1px solid rgba(59, 130, 246, 0.3);
}
.status-badge.status-close_wait,
.status-badge.status-time_wait {
background: rgba(251, 146, 60, 0.2);
color: var(--accent-warning);
border: 1px solid rgba(251, 146, 60, 0.3);
}
.status-badge.status-closed {
background: rgba(148, 163, 184, 0.2);
color: var(--text-muted);
border: 1px solid rgba(148, 163, 184, 0.3);
}
/* 流量信息 */
.traffic-info {
color: var(--accent-secondary);
font-weight: 500;
}
/* 空状态 */
.empty-table {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 24px;
color: var(--text-muted);
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 14px;
}
</style>
@@ -0,0 +1,310 @@
<template>
<div class="timeline-area" :style="{ height: height + 'px' }">
<div class="timeline-header">
<h3 class="timeline-title">
<span class="title-icon">📈</span>
实时事件流
<span class="event-count">{{ events.length }} 个事件</span>
</h3>
<div class="timeline-controls">
<n-button
size="small"
:type="isPaused ? 'info' : 'warning'"
ghost
@click="$emit('pause')"
>
{{ isPaused ? '继续' : '暂停' }}
</n-button>
<n-button size="small" ghost @click="$emit('clear')">清空</n-button>
</div>
</div>
<div class="timeline-content scrollbar-warning scrollbar-animated">
<div class="events-container">
<transition-group name="event-list" tag="div">
<div
v-for="event in displayEvents"
:key="event.id"
class="event-item"
:class="[`event-${event.type}`, { 'event-new': event.isNew }]"
>
<div class="event-time">{{ event.time }}</div>
<div class="event-content">
<div class="event-type">{{ event.eventType }}</div>
<div class="event-desc">{{ event.description }}</div>
</div>
<div class="event-indicator"></div>
</div>
</transition-group>
<!-- 空状态 -->
<div v-if="events.length === 0" class="empty-events">
<div class="empty-icon">📭</div>
<div class="empty-text">暂无事件</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { NButton } from 'naive-ui'
// Props
const props = defineProps<{
height: number
events: Array<{
id: number
time: string
type: 'info' | 'warning' | 'success' | 'error'
eventType: string
description: string
}>
isPaused: boolean
}>()
// Emits
const emit = defineEmits<{
pause: []
clear: []
}>()
// 显示的事件(添加动画标记)
const displayEvents = ref(props.events.map(e => ({ ...e, isNew: false })))
// 监听事件变化,标记新事件
watch(() => props.events, (newEvents, oldEvents) => {
if (!props.isPaused) {
const oldIds = new Set(oldEvents?.map(e => e.id) || [])
displayEvents.value = newEvents.map(e => ({
...e,
isNew: !oldIds.has(e.id)
}))
// 500ms后移除新事件标记
setTimeout(() => {
displayEvents.value = displayEvents.value.map(e => ({ ...e, isNew: false }))
}, 500)
}
}, { deep: true })
</script>
<style scoped>
/* 时间轴容器 */
.timeline-area {
background: var(--bg-glass);
backdrop-filter: var(--backdrop-blur);
border-top: 1px solid var(--border-primary);
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
/* 头部 */
.timeline-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-secondary);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-card);
flex-shrink: 0;
}
.timeline-title {
font-size: 16px;
font-weight: 600;
color: var(--text-secondary);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.title-icon {
font-size: 16px;
}
.event-count {
font-size: 12px;
color: var(--text-muted);
font-weight: 400;
margin-left: 8px;
}
.timeline-controls {
display: flex;
gap: 8px;
}
/* 内容区域 */
.timeline-content {
flex: 1;
overflow-y: auto;
padding: 12px 24px;
min-height: 0;
}
.events-container {
display: flex;
flex-direction: column;
gap: 8px;
}
/* 事件项 */
.event-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-card);
border-radius: 8px;
border-left: 3px solid transparent;
transition: var(--transition);
position: relative;
}
.event-item:hover {
background: var(--bg-hover);
}
.event-item.event-new {
animation: slideIn 0.3s ease-out;
}
/* 事件类型样式 */
.event-item.event-info {
border-left-color: var(--accent-primary);
}
.event-item.event-warning {
border-left-color: var(--accent-warning);
}
.event-item.event-success {
border-left-color: var(--accent-success);
}
.event-item.event-error {
border-left-color: var(--accent-error);
}
/* 事件内容 */
.event-time {
font-size: 11px;
color: var(--text-muted);
min-width: 70px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.event-content {
flex: 1;
}
.event-type {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
line-height: 1;
}
.event-desc {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
/* 事件指示器 */
.event-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-disabled);
}
.event-item.event-info .event-indicator {
background: var(--accent-primary);
animation: pulse 2s infinite;
}
.event-item.event-warning .event-indicator {
background: var(--accent-warning);
animation: pulse 2s infinite;
}
.event-item.event-success .event-indicator {
background: var(--accent-success);
animation: pulse 2s infinite;
}
.event-item.event-error .event-indicator {
background: var(--accent-error);
animation: pulse 2s infinite;
}
/* 空状态 */
.empty-events {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--text-muted);
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 14px;
}
/* 动画 */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 currentColor;
}
70% {
box-shadow: 0 0 0 4px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
/* 列表过渡动画 */
.event-list-enter-active,
.event-list-leave-active {
transition: all 0.3s ease;
}
.event-list-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.event-list-leave-to {
opacity: 0;
transform: translateX(20px);
}
.event-list-move {
transition: transform 0.3s ease;
}
</style>
@@ -0,0 +1,235 @@
<template>
<div class="traffic-chart">
<div class="chart-header">
<span class="chart-title">实时流量</span>
<span class="chart-value">{{ currentSpeed }}</span>
</div>
<div class="chart-container">
<div class="chart-bars">
<div
v-for="(value, index) in data"
:key="index"
class="chart-bar"
:style="{ height: value + '%' }"
:class="{ 'bar-active': index === data.length - 1 }"
>
<div class="bar-tooltip">{{ formatSpeed(value) }}</div>
</div>
</div>
<div class="chart-grid">
<div class="grid-line" v-for="i in 4" :key="i"></div>
</div>
</div>
<div class="chart-footer">
<span class="chart-label">{{ timeRange }}</span>
<div class="chart-legend">
<span class="legend-item upload">
<span class="legend-dot"></span>
上传
</span>
<span class="legend-item download">
<span class="legend-dot"></span>
下载
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
// Props
const props = defineProps<{
data: number[]
}>()
// 当前速度
const currentSpeed = computed(() => {
const lastValue = props.data[props.data.length - 1] || 0
return formatSpeed(lastValue)
})
// 时间范围
const timeRange = ref('最近 60 秒')
// 格式化速度
const formatSpeed = (value: number) => {
const speed = (value / 100) * 10 // 假设最大10MB/s
if (speed < 1) {
return `${(speed * 1024).toFixed(0)} KB/s`
}
return `${speed.toFixed(1)} MB/s`
}
// 动画定时器
let animationTimer: number | null = null
onMounted(() => {
// 添加入场动画
animationTimer = window.setTimeout(() => {
document.querySelectorAll('.chart-bar').forEach((el, index) => {
;(el as HTMLElement).style.transitionDelay = `${index * 20}ms`
})
}, 100)
})
onUnmounted(() => {
if (animationTimer) {
clearTimeout(animationTimer)
}
})
</script>
<style scoped>
/* 图表容器 */
.traffic-chart {
background: var(--bg-card);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--border-tertiary);
}
/* 图表头部 */
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.chart-title {
font-size: 12px;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chart-value {
font-size: 14px;
font-weight: 700;
color: var(--accent-secondary);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
/* 图表主体 */
.chart-container {
position: relative;
height: 80px;
margin-bottom: 8px;
}
.chart-bars {
display: flex;
align-items: flex-end;
gap: 2px;
height: 100%;
position: relative;
z-index: 2;
}
.chart-bar {
flex: 1;
background: linear-gradient(to top, var(--accent-primary), var(--accent-secondary));
border-radius: 2px 2px 0 0;
min-height: 2px;
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
cursor: pointer;
}
.chart-bar:hover {
background: linear-gradient(to top, #1d4ed8, #0891b2);
}
.chart-bar.bar-active {
background: linear-gradient(to top, var(--accent-warning), var(--accent-purple));
}
/* 工具提示 */
.bar-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-4px);
background: var(--bg-tooltip);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.chart-bar:hover .bar-tooltip {
opacity: 1;
}
/* 网格线 */
.chart-grid {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
pointer-events: none;
}
.grid-line {
height: 1px;
background: var(--border-secondary);
opacity: 0.5;
}
/* 图表底部 */
.chart-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-label {
font-size: 11px;
color: var(--text-muted);
}
/* 图例 */
.chart-legend {
display: flex;
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-muted);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-primary);
}
.legend-item.upload .legend-dot {
background: var(--accent-warning);
}
.legend-item.download .legend-dot {
background: var(--accent-secondary);
}
/* 响应式 */
@media (max-width: 1400px) {
.chart-container {
height: 60px;
}
}
</style>
@@ -0,0 +1,249 @@
<template>
<div class="monitor-view">
<!-- 主内容区域 - 可拖拽布局 -->
<div class="main-content">
<!-- 左侧边栏 -->
<MonitorSidebar :width="sidebarWidth" :data="mockData" />
<!-- 左侧分割线 -->
<div class="resize-handle-vertical" @mousedown="startResize('sidebar', $event)">
<div class="resize-handle-hover"></div>
</div>
<!-- 中间主视图区域 -->
<MonitorMainPanel
:headerHeight="mainHeaderHeight"
:timelineHeight="timelineHeight"
:searchQuery="searchQuery"
:connections="mockConnections"
:events="mockEvents"
@update:searchQuery="searchQuery = $event"
@resizeTimeline="startResize('timeline', $event)"
@selectConnection="handleConnectionSelect"
/>
<!-- 右侧分割线 -->
<div class="resize-handle-vertical" @mousedown="startResize('inspector', $event)">
<div class="resize-handle-hover"></div>
</div>
<!-- 右侧检查器面板 -->
<MonitorInspector :width="inspectorWidth" :selectedConnection="selectedConnection" />
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'MonitorView',
})
import { ref, onMounted, onUnmounted, provide } from 'vue'
import MonitorSidebar from './MonitorSidebar.vue'
import MonitorMainPanel from './MonitorMainPanel.vue'
import MonitorInspector from './MonitorInspector.vue'
// 布局尺寸控制
const sidebarWidth = ref(300)
const inspectorWidth = ref(350)
const timelineHeight = ref(500)
const mainHeaderHeight = ref(80)
// 调整分割条范围限制
const MIN_SIDEBAR_WIDTH = 200
const MAX_SIDEBAR_WIDTH = 600
const MIN_INSPECTOR_WIDTH = 250
const MAX_INSPECTOR_WIDTH = 650
const MIN_TIMELINE_HEIGHT = 100
const MAX_TIMELINE_HEIGHT = 800
// 响应式数据
const searchQuery = ref('')
const selectedConnection = ref<any>(null)
// 模拟数据
const mockData = ref({
activeConnections: 247,
processCount: 43,
networkSpeed: '5.2 MB/s',
ruleCount: 156,
})
const mockConnections = ref([
{
id: 1,
process: 'chrome.exe',
localAddress: '192.168.1.100',
remoteAddress: 'google.com',
status: 'established',
traffic: '542 KB/s',
},
// ... 更多连接数据
])
const mockEvents = ref([
{
id: 1,
time: new Date().toLocaleTimeString(),
type: 'connection',
eventType: '连接建立',
description: 'chrome.exe → google.com:443',
},
// ... 更多事件数据
])
// 提供布局配置给子组件
provide('layoutConfig', {
sidebarWidth,
inspectorWidth,
timelineHeight,
mainHeaderHeight,
})
// 拖拽状态
const resizing = ref<{
type: 'sidebar' | 'inspector' | 'timeline' | null
startX: number
startY: number
startWidth: number
startHeight: number
}>({
type: null,
startX: 0,
startY: 0,
startWidth: 0,
startHeight: 0,
})
// 开始拖拽
const startResize = (type: 'sidebar' | 'inspector' | 'timeline', event: MouseEvent) => {
event.preventDefault()
resizing.value = {
type,
startX: event.clientX,
startY: event.clientY,
startWidth:
type === 'sidebar' ? sidebarWidth.value : type === 'inspector' ? inspectorWidth.value : 0,
startHeight: type === 'timeline' ? timelineHeight.value : 0,
}
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', stopResize)
document.body.style.cursor = type === 'timeline' ? 'row-resize' : 'col-resize'
document.body.style.userSelect = 'none'
}
// 处理拖拽
const handleResize = (event: MouseEvent) => {
if (!resizing.value.type) return
const { type, startX, startY, startWidth, startHeight } = resizing.value
if (type === 'sidebar') {
const deltaX = event.clientX - startX
const newWidth = Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, startWidth + deltaX))
sidebarWidth.value = newWidth
} else if (type === 'inspector') {
const deltaX = startX - event.clientX
const newWidth = Math.min(
MAX_INSPECTOR_WIDTH,
Math.max(MIN_INSPECTOR_WIDTH, startWidth + deltaX),
)
inspectorWidth.value = newWidth
} else if (type === 'timeline') {
const deltaY = startY - event.clientY
const newHeight = Math.min(
MAX_TIMELINE_HEIGHT,
Math.max(MIN_TIMELINE_HEIGHT, startHeight + deltaY),
)
timelineHeight.value = newHeight
}
}
// 停止拖拽
const stopResize = () => {
resizing.value.type = null
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
// 处理连接选中
const handleConnectionSelect = (connection: any) => {
selectedConnection.value = connection
}
// 重置布局
const resetLayout = () => {
sidebarWidth.value = 300
inspectorWidth.value = 350
timelineHeight.value = 200
}
// 键盘快捷键
onMounted(() => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === 'r') {
event.preventDefault()
resetLayout()
}
}
document.addEventListener('keydown', handleKeydown)
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
})
})
</script>
<style scoped>
/* 监控视图容器 */
.monitor-view {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
min-height: 0;
overflow: hidden;
}
/* 拖拽分割线 */
.resize-handle-vertical {
width: 4px;
background: var(--border-tertiary);
cursor: col-resize;
transition: background-color 0.2s;
position: relative;
z-index: 10;
flex-shrink: 0;
}
.resize-handle-vertical:hover {
background: var(--border-hover);
}
.resize-handle-hover {
position: absolute;
inset: 0 -4px;
}
/* 拖拽时的视觉反馈 */
.resize-handle-vertical:active {
background-color: var(--accent-primary) !important;
}
</style>