U:调整合并所有颜色样式;拆分MonitorView组件
This commit is contained in:
1 parent
c374165dcb
commit
c9a876d5da
13 files changed
+2348
-262
No files matched your search
@@ -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; }
|
||||
|
||||
/* ==================== 表单元素基础样式 ==================== */
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 主题切换过渡效果 ==================== */
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 引入主题变量(放在其他样式之前)
|
||||
import './assets/theme-variables.css'
|
||||
import './assets/main.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
@@ -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>
|
||||
File renamed without changes.
Reference in new issue
Block a user