A:提交基础的页面布局;划分顶部标题栏组件
U:修复主题修改样式部分样式无法调整的BUG
This commit is contained in:
1 parent
e55843eae2
commit
329e4f066a
16 files changed
+1993
-1560
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -4,7 +4,7 @@ import PathUtils from '@/utils/pathUtils'
|
|||||||
import StringUtils from '@/utils/stringUtils'
|
import StringUtils from '@/utils/stringUtils'
|
||||||
import type { subRoute, subRouteList } from '@/types/devIndex'
|
import type { subRoute, subRouteList } from '@/types/devIndex'
|
||||||
import app from '@/constants/app'
|
import app from '@/constants/app'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, provide } from 'vue'
|
||||||
import { NButton } from 'naive-ui'
|
import { NButton } from 'naive-ui'
|
||||||
import { NConfigProvider, NGlobalStyle } from 'naive-ui'
|
import { NConfigProvider, NGlobalStyle } from 'naive-ui'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
@@ -13,12 +13,10 @@ const router = useRouter()
|
|||||||
// 是否展示生成路由
|
// 是否展示生成路由
|
||||||
const isShowGenerateRouter = ref(false)
|
const isShowGenerateRouter = ref(false)
|
||||||
|
|
||||||
// 添加拖拽相关状态
|
|
||||||
const isDragging = ref(false)
|
|
||||||
const navPosition = ref({ x: 10, y: 10 }) // 默认位置
|
|
||||||
const dragOffset = ref({ x: 0, y: 0 })
|
|
||||||
const navRef = ref(null) // 用于获取导航栏元素
|
const navRef = ref(null) // 用于获取导航栏元素
|
||||||
|
|
||||||
|
console.log('App.Vue')
|
||||||
|
|
||||||
// region 获取路由列表
|
// region 获取路由列表
|
||||||
// 自动获取路由列表
|
// 自动获取路由列表
|
||||||
const routes = {
|
const routes = {
|
||||||
@@ -73,7 +71,19 @@ if (nodeEnv !== undefined && !StringUtils.isBlank(nodeEnv) && nodeEnv === app.DE
|
|||||||
|
|
||||||
// 主题样式
|
// 主题样式
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
const theme = themeStore.theme
|
// 提供主题给子组件
|
||||||
|
provide('theme', themeStore.theme)
|
||||||
|
// 初始化主题
|
||||||
|
onMounted(() => {
|
||||||
|
themeStore.initTheme()
|
||||||
|
// 标记页面已准备好,启用过渡动画
|
||||||
|
document.body.classList.add('theme-ready')
|
||||||
|
})
|
||||||
|
// region 拖拽 tab
|
||||||
|
// 添加拖拽相关状态
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const navPosition = ref({ x: 10, y: 10 }) // 默认位置
|
||||||
|
const dragOffset = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
// 拖拽相关函数
|
// 拖拽相关函数
|
||||||
function startDrag(event) {
|
function startDrag(event) {
|
||||||
@@ -108,6 +118,8 @@ function endDrag() {
|
|||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
// 添加全局事件监听
|
// 添加全局事件监听
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('mousemove', onDrag)
|
window.addEventListener('mousemove', onDrag)
|
||||||
@@ -122,7 +134,11 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider class="app-wrapper" :theme="theme">
|
<n-config-provider
|
||||||
|
class="app-wrapper"
|
||||||
|
:theme-overrides="themeStore.themeOverrides"
|
||||||
|
:theme="themeStore.theme"
|
||||||
|
>
|
||||||
<n-global-style />
|
<n-global-style />
|
||||||
|
|
||||||
<!-- 可拖动的导航栏 -->
|
<!-- 可拖动的导航栏 -->
|
||||||
|
|||||||
@@ -1,5 +1,29 @@
|
|||||||
@import './base.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{
|
li{
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<div class="title-bar">
|
||||||
|
<div class="title-bar-left">
|
||||||
|
<div class="app-title">
|
||||||
|
<div class="app-icon">
|
||||||
|
<span>N</span>
|
||||||
|
</div>
|
||||||
|
<span class="app-name">NetVisor</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view-switcher">
|
||||||
|
<n-tabs
|
||||||
|
type="segment"
|
||||||
|
class="n-tab-group-set"
|
||||||
|
animated
|
||||||
|
:value="activeTab"
|
||||||
|
@update:value="handleTabChange"
|
||||||
|
>
|
||||||
|
<n-tab
|
||||||
|
class="n-tab-item-set"
|
||||||
|
v-for="tab in tabConfig"
|
||||||
|
:key="tab.name"
|
||||||
|
:name="tab.name"
|
||||||
|
:tab="tab.label"
|
||||||
|
/>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title-bar-right">
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot active"></div>
|
||||||
|
<span>实时监控中</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-button circle size="small" quaternary @click="openSettings">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="SettingsOutline" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button circle size="small" quaternary @click="toggleDarkMode">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="isDark ? SunnyOutline : MoonOutline" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NIcon, NTabs, NTab } from 'naive-ui'
|
||||||
|
import { SettingsOutline, MoonOutline, SunnyOutline } from '@vicons/ionicons5'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
// region 路由的激活和绑定
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// Tab配置
|
||||||
|
const tabConfig = [
|
||||||
|
{ name: 'monitor', label: '实时监控', path: '/monitor' },
|
||||||
|
{ name: 'firewall', label: '防火墙', path: '/firewall' },
|
||||||
|
{ name: 'analyse', label: '分析', path: '/analyse' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 根据当前路由设置激活的Tab
|
||||||
|
const activeTab = ref(route.name || 'monitor')
|
||||||
|
|
||||||
|
// 监听路由变化,同步Tab状态
|
||||||
|
watch(
|
||||||
|
() => route.name,
|
||||||
|
(newRouteName) => {
|
||||||
|
if (newRouteName) {
|
||||||
|
activeTab.value = newRouteName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 处理Tab切换
|
||||||
|
const handleTabChange = (tabName: string) => {
|
||||||
|
activeTab.value = tabName
|
||||||
|
router.push({ name: tabName })
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region 黑暗模式
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
const isDark = computed(() => themeStore.isDark)
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
themeStore.toggleTheme()
|
||||||
|
console.log('切换暗色模式:', isDark.value)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region 打开设置
|
||||||
|
const openSettings = () => {
|
||||||
|
console.log('打开设置')
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 自定义 Naive UI Tabs 组件的主题样式 */
|
||||||
|
.view-switcher {
|
||||||
|
--n-tab-text-color: var(--text-quaternary);
|
||||||
|
--n-tab-text-color-active: var(--text-primary);
|
||||||
|
--n-tab-text-color-hover: var(--text-secondary);
|
||||||
|
--n-tab-color-segment: var(--bg-card);
|
||||||
|
--n-tab-border-color: var(--border-tertiary);
|
||||||
|
--n-bar-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强制覆盖 n-tabs 的样式 */
|
||||||
|
.n-tab-group-set {
|
||||||
|
/* 段落式 tabs 的背景色 */
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab-wrapper) {
|
||||||
|
background-color: var(--bg-card) !important;
|
||||||
|
border-color: var(--border-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tab 项的文字颜色 */
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab) {
|
||||||
|
color: var(--text-quaternary) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.n-tabs-tab--active {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
background-color: var(--bg-overlay) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 段落式 tabs 的活动指示器 */
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab-pad) {
|
||||||
|
background-color: var(--bg-overlay) !important;
|
||||||
|
border-color: var(--border-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保主题切换时的过渡效果 */
|
||||||
|
.view-switcher * {
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部标题栏 */
|
||||||
|
.title-bar {
|
||||||
|
height: 60px;
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
|
background: var(--bg-overlay);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tab 栏样式微调 */
|
||||||
|
.n-tab-group-set {
|
||||||
|
width: 30vw;
|
||||||
|
min-width: 270px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加过渡动画,减少闪烁 */
|
||||||
|
.view-switcher {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg, var(--accent-primary) 0%, #1d4ed8 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-quaternary);
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-success);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.title-bar {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-left {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,7 +27,7 @@ const app = {
|
|||||||
/**
|
/**
|
||||||
* 是否展示生成路由
|
* 是否展示生成路由
|
||||||
*/
|
*/
|
||||||
IS_SHOW_GENERATE_ROUTER: true
|
IS_SHOW_GENERATE_ROUTER: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
@@ -9,18 +9,25 @@ import CSharpBridgeV2 from '@/correspond/CSharpBridgeV2'
|
|||||||
import websocketPlugin from '@/plugins/websocketPlugin'
|
import websocketPlugin from '@/plugins/websocketPlugin'
|
||||||
import { useWebSocketStore } from '@/stores/websocketStore'
|
import { useWebSocketStore } from '@/stores/websocketStore'
|
||||||
import { createNaiveUI } from './naive'
|
import { createNaiveUI } from './naive'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
|
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
app.mount('#app')
|
|
||||||
app.use(websocketPlugin)
|
app.use(websocketPlugin)
|
||||||
|
|
||||||
|
|
||||||
// 使用 主题 支持
|
// 使用 主题 支持
|
||||||
app.use(createNaiveUI())
|
app.use(createNaiveUI())
|
||||||
|
|
||||||
|
// 这行代码会触发 store 的初始化,从而立即应用主题
|
||||||
|
useThemeStore()
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
// 注册 C# 函数
|
// 注册 C# 函数
|
||||||
window.externalFunctions = {}
|
window.externalFunctions = {}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import StringUtils from '@/utils/stringUtils'
|
|||||||
import PathUtils from '@/utils/pathUtils'
|
import PathUtils from '@/utils/pathUtils'
|
||||||
import NotFound from '@/views/NotFoundView.vue'
|
import NotFound from '@/views/NotFoundView.vue'
|
||||||
import AboutView from '@/views/AboutView.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'
|
||||||
|
|
||||||
// 导入所有 .vue 文件
|
// 导入所有 .vue 文件
|
||||||
const routes = import.meta.glob('@/views/dev/**/*.vue')
|
const routes = import.meta.glob('@/views/dev/**/*.vue')
|
||||||
@@ -43,7 +46,7 @@ const router = createRouter({
|
|||||||
// component: HomeView,
|
// component: HomeView,
|
||||||
// children: children,
|
// children: children,
|
||||||
// 重定向到主页
|
// 重定向到主页
|
||||||
redirect: '/home',
|
redirect: '/monitor',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/home',
|
path: '/home',
|
||||||
@@ -56,6 +59,24 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 实时监控
|
||||||
|
{
|
||||||
|
path: '/monitor',
|
||||||
|
name: 'monitor',
|
||||||
|
component: MonitorView,
|
||||||
|
},
|
||||||
|
// 防火墙
|
||||||
|
{
|
||||||
|
path: '/firewall',
|
||||||
|
name: 'firewall',
|
||||||
|
component: FirewallView,
|
||||||
|
},
|
||||||
|
// 网络分析
|
||||||
|
{
|
||||||
|
path: '/analyse',
|
||||||
|
name: 'analyse',
|
||||||
|
component: AnalyseView,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/dev',
|
path: '/dev',
|
||||||
|
|||||||
@@ -1,30 +1,194 @@
|
|||||||
// stores/theme.ts
|
// stores/theme.ts - 完善的主题存储
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { darkTheme, lightTheme } from 'naive-ui'
|
import { darkTheme, lightTheme } from 'naive-ui'
|
||||||
|
import type { GlobalTheme, GlobalThemeOverrides } from 'naive-ui'
|
||||||
|
|
||||||
export const useThemeStore = defineStore('theme', () => {
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
// 初始读取系统设置或本地缓存
|
// 主题状态
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
|
const isDark = ref(false)
|
||||||
const saved = localStorage.getItem('theme')
|
|
||||||
const isDark = ref(saved ? saved === 'dark' : prefersDark.matches)
|
|
||||||
|
|
||||||
// 当前 Naive UI 的主题对象
|
// 初始化主题(从本地存储读取)
|
||||||
const theme = computed(() => (isDark.value ? darkTheme : lightTheme))
|
const initTheme = () => {
|
||||||
|
const savedTheme = localStorage.getItem('theme')
|
||||||
|
isDark.value = savedTheme === 'dark'
|
||||||
|
|
||||||
// 切换主题
|
// 同步到 HTML 元素
|
||||||
function toggleTheme() {
|
if (isDark.value) {
|
||||||
isDark.value = !isDark.value
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动保存主题状态
|
// 切换主题
|
||||||
watch(isDark, (val) => {
|
const toggleTheme = () => {
|
||||||
localStorage.setItem('theme', val ? 'dark' : 'light')
|
isDark.value = !isDark.value
|
||||||
|
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||||
|
|
||||||
|
// 同步到 HTML 元素
|
||||||
|
if (isDark.value) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Naive UI 主题
|
||||||
|
const theme = computed<GlobalTheme | null>(() => {
|
||||||
|
return isDark.value ? darkTheme : null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 主题覆盖配置
|
||||||
|
const themeOverrides = computed<GlobalThemeOverrides>(() => {
|
||||||
|
if (isDark.value) {
|
||||||
|
// 暗色主题配置
|
||||||
|
return {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#3b82f6',
|
||||||
|
primaryColorHover: '#60a5fa',
|
||||||
|
primaryColorPressed: '#2563eb',
|
||||||
|
primaryColorSuppl: '#1d4ed8',
|
||||||
|
|
||||||
|
infoColor: '#06b6d4',
|
||||||
|
infoColorHover: '#22d3ee',
|
||||||
|
infoColorPressed: '#0891b2',
|
||||||
|
|
||||||
|
successColor: '#22c55e',
|
||||||
|
successColorHover: '#4ade80',
|
||||||
|
successColorPressed: '#16a34a',
|
||||||
|
|
||||||
|
warningColor: '#f59e0b',
|
||||||
|
warningColorHover: '#fbbf24',
|
||||||
|
warningColorPressed: '#d97706',
|
||||||
|
|
||||||
|
errorColor: '#ef4444',
|
||||||
|
errorColorHover: '#f87171',
|
||||||
|
errorColorPressed: '#dc2626',
|
||||||
|
|
||||||
|
textColorBase: '#f8fafc',
|
||||||
|
textColor1: '#f1f5f9',
|
||||||
|
textColor2: '#e2e8f0',
|
||||||
|
textColor3: '#cbd5e1',
|
||||||
|
|
||||||
|
dividerColor: '#334155',
|
||||||
|
borderColor: '#334155',
|
||||||
|
|
||||||
|
popoverColor: '#1e293b',
|
||||||
|
cardColor: '#1e293b',
|
||||||
|
modalColor: '#1e293b',
|
||||||
|
bodyColor: '#0f172a',
|
||||||
|
|
||||||
|
tableColor: '#1e293b',
|
||||||
|
tableColorHover: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
tableColorStriped: '#334155',
|
||||||
|
},
|
||||||
|
|
||||||
|
Tabs: {
|
||||||
|
colorSegment: 'rgba(51, 65, 85, 0.5)',
|
||||||
|
tabTextColorSegment: '#cbd5e1',
|
||||||
|
tabTextColorActiveSegment: '#f8fafc',
|
||||||
|
tabTextColorHoverSegment: '#f1f5f9',
|
||||||
|
tabColorSegment: 'rgba(15, 23, 42, 0.95)',
|
||||||
|
tabBorderColor: '#334155',
|
||||||
|
barColor: '#3b82f6',
|
||||||
|
},
|
||||||
|
|
||||||
|
Button: {
|
||||||
|
textColorGhost: '#cbd5e1',
|
||||||
|
textColorGhostHover: '#f1f5f9',
|
||||||
|
textColorGhostPressed: '#e2e8f0',
|
||||||
|
},
|
||||||
|
|
||||||
|
Input: {
|
||||||
|
color: 'rgba(51, 65, 85, 0.5)',
|
||||||
|
colorFocus: 'rgba(51, 65, 85, 0.8)',
|
||||||
|
textColor: '#f1f5f9',
|
||||||
|
caretColor: '#3b82f6',
|
||||||
|
border: '1px solid #334155',
|
||||||
|
borderHover: '1px solid #475569',
|
||||||
|
borderFocus: '1px solid #3b82f6',
|
||||||
|
placeholderColor: '#64748b',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 亮色主题配置
|
||||||
|
return {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#3b82f6',
|
||||||
|
primaryColorHover: '#2563eb',
|
||||||
|
primaryColorPressed: '#1d4ed8',
|
||||||
|
primaryColorSuppl: '#60a5fa',
|
||||||
|
|
||||||
|
infoColor: '#06b6d4',
|
||||||
|
infoColorHover: '#0891b2',
|
||||||
|
infoColorPressed: '#0e7490',
|
||||||
|
|
||||||
|
successColor: '#22c55e',
|
||||||
|
successColorHover: '#16a34a',
|
||||||
|
successColorPressed: '#15803d',
|
||||||
|
|
||||||
|
warningColor: '#f59e0b',
|
||||||
|
warningColorHover: '#d97706',
|
||||||
|
warningColorPressed: '#b45309',
|
||||||
|
|
||||||
|
errorColor: '#ef4444',
|
||||||
|
errorColorHover: '#dc2626',
|
||||||
|
errorColorPressed: '#b91c1c',
|
||||||
|
|
||||||
|
textColorBase: '#0f172a',
|
||||||
|
textColor1: '#1e293b',
|
||||||
|
textColor2: '#334155',
|
||||||
|
textColor3: '#475569',
|
||||||
|
|
||||||
|
dividerColor: '#e2e8f0',
|
||||||
|
borderColor: '#e2e8f0',
|
||||||
|
|
||||||
|
popoverColor: '#ffffff',
|
||||||
|
cardColor: '#ffffff',
|
||||||
|
modalColor: '#ffffff',
|
||||||
|
bodyColor: '#f8fafc',
|
||||||
|
|
||||||
|
tableColor: '#ffffff',
|
||||||
|
tableColorHover: 'rgba(59, 130, 246, 0.08)',
|
||||||
|
tableColorStriped: '#f8fafc',
|
||||||
|
},
|
||||||
|
|
||||||
|
Tabs: {
|
||||||
|
colorSegment: 'rgba(241, 245, 249, 0.8)',
|
||||||
|
tabTextColorSegment: '#475569',
|
||||||
|
tabTextColorActiveSegment: '#0f172a',
|
||||||
|
tabTextColorHoverSegment: '#1e293b',
|
||||||
|
tabColorSegment: '#ffffff',
|
||||||
|
tabBorderColor: '#e2e8f0',
|
||||||
|
barColor: '#3b82f6',
|
||||||
|
},
|
||||||
|
|
||||||
|
Button: {
|
||||||
|
textColorGhost: '#475569',
|
||||||
|
textColorGhostHover: '#1e293b',
|
||||||
|
textColorGhostPressed: '#334155',
|
||||||
|
},
|
||||||
|
|
||||||
|
Input: {
|
||||||
|
color: 'rgba(241, 245, 249, 0.8)',
|
||||||
|
colorFocus: '#ffffff',
|
||||||
|
textColor: '#1e293b',
|
||||||
|
caretColor: '#3b82f6',
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
borderHover: '1px solid #cbd5e1',
|
||||||
|
borderFocus: '1px solid #3b82f6',
|
||||||
|
placeholderColor: '#94a3b8',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDark,
|
isDark,
|
||||||
theme,
|
theme,
|
||||||
toggleTheme,
|
themeOverrides,
|
||||||
|
initTheme,
|
||||||
|
toggleTheme
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>网络分析</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>防火墙</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,1536 +1,3 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="appContainer" class="app-container">
|
<h1>HomeView</h1>
|
||||||
<!-- 顶部标题栏 -->
|
|
||||||
<div class="title-bar">
|
|
||||||
<div class="title-bar-left">
|
|
||||||
<div class="app-title">
|
|
||||||
<div class="app-icon">
|
|
||||||
<span>N</span>
|
|
||||||
</div>
|
|
||||||
<span class="app-name">NetGuard</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="view-switcher">
|
|
||||||
<n-radio-group v-model:value="currentView" size="small">
|
|
||||||
<n-radio-button v-for="option in viewOptions" :key="option.value" :value="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</n-radio-button>
|
|
||||||
</n-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="title-bar-right">
|
|
||||||
<div class="status-indicator">
|
|
||||||
<div class="status-dot active"></div>
|
|
||||||
<span>实时监控中</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-button circle size="small" quaternary @click="openSettings">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="SettingsOutline" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
|
|
||||||
<n-button circle size="small" quaternary @click="toggleDarkMode">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="isDark ? SunnyOutline : MoonOutline" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主内容区域 - 可拖拽布局 -->
|
|
||||||
<div class="main-content">
|
|
||||||
<!-- 左侧边栏 -->
|
|
||||||
<div class="sidebar" :style="{ width: sidebarWidth + 'px' }">
|
|
||||||
<div class="sidebar-content">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h3 class="sidebar-title">系统概览</h3>
|
|
||||||
<div class="sidebar-badge">{{ mockData.processCount }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-stats">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon">🔗</div>
|
|
||||||
<div class="stat-info">
|
|
||||||
<div class="stat-value">{{ mockData.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">{{ mockData.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">{{ mockData.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">扫描威胁</div>
|
|
||||||
<div class="action-item">更新规则</div>
|
|
||||||
<div class="action-item">导出日志</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 左侧分割线 -->
|
|
||||||
<div class="resize-handle-vertical" @mousedown="startResize('sidebar', $event)">
|
|
||||||
<div class="resize-handle-hover"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 中间主视图区域 -->
|
|
||||||
<div class="main-view">
|
|
||||||
<!-- 中间顶部 - 搜索和标题 -->
|
|
||||||
<div class="main-header" :style="{ height: mainHeaderHeight + '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
|
|
||||||
v-model:value="searchQuery"
|
|
||||||
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>过滤</n-button>
|
|
||||||
<n-button size="small" ghost>刷新</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 中间主体 - 连接列表表格 -->
|
|
||||||
<div class="connections-area">
|
|
||||||
<div class="connections-header">
|
|
||||||
<h3 class="connections-title">
|
|
||||||
<span class="title-icon">📊</span>
|
|
||||||
连接列表
|
|
||||||
<span class="connection-count">{{ mockData.activeConnections }} 个活跃连接</span>
|
|
||||||
</h3>
|
|
||||||
<div class="connections-controls">
|
|
||||||
<n-button size="small" type="info" ghost>暂停监控</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="connections-content">
|
|
||||||
<div class="table-container">
|
|
||||||
<div class="table-header">
|
|
||||||
<div class="table-column">进程</div>
|
|
||||||
<div class="table-column">本地地址</div>
|
|
||||||
<div class="table-column">远程地址</div>
|
|
||||||
<div class="table-column">状态</div>
|
|
||||||
<div class="table-column">流量</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-body">
|
|
||||||
<div
|
|
||||||
v-for="i in 15"
|
|
||||||
:key="i"
|
|
||||||
class="table-row"
|
|
||||||
:class="{ 'row-selected': i === 3 }"
|
|
||||||
>
|
|
||||||
<div class="table-cell">
|
|
||||||
<div class="process-info">
|
|
||||||
<div class="process-icon">🌐</div>
|
|
||||||
<span>chrome.exe</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-cell">192.168.1.{{ 100 + i }}</div>
|
|
||||||
<div class="table-cell">
|
|
||||||
{{ ['google.com', 'github.com', 'cloudflare.com'][i % 3] }}
|
|
||||||
</div>
|
|
||||||
<div class="table-cell">
|
|
||||||
<span class="status-badge established">已建立</span>
|
|
||||||
</div>
|
|
||||||
<div class="table-cell">
|
|
||||||
<span class="traffic-info">{{ (Math.random() * 1000).toFixed(0) }} KB/s</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 时间轴分割线 -->
|
|
||||||
<div class="resize-handle-horizontal" @mousedown="startResize('timeline', $event)">
|
|
||||||
<div class="resize-handle-hover-h"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 中间底部 - 时间轴事件流 -->
|
|
||||||
<div class="timeline-area" :style="{ height: timelineHeight + 'px' }">
|
|
||||||
<div class="timeline-header">
|
|
||||||
<h3 class="timeline-title">
|
|
||||||
<span class="title-icon">📈</span>
|
|
||||||
实时事件流
|
|
||||||
</h3>
|
|
||||||
<div class="timeline-controls">
|
|
||||||
<n-button size="small" type="warning" ghost>暂停</n-button>
|
|
||||||
<n-button size="small" ghost>清空</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timeline-content">
|
|
||||||
<div class="events-container">
|
|
||||||
<div
|
|
||||||
v-for="i in 12"
|
|
||||||
:key="i"
|
|
||||||
class="event-item"
|
|
||||||
:class="['event-' + ['info', 'warning', 'success'][i % 3]]"
|
|
||||||
>
|
|
||||||
<div class="event-time">{{ new Date().toLocaleTimeString() }}</div>
|
|
||||||
<div class="event-content">
|
|
||||||
<div class="event-type">{{ ['连接建立', '数据传输', '连接关闭'][i % 3] }}</div>
|
|
||||||
<div class="event-desc">chrome.exe → google.com:443</div>
|
|
||||||
</div>
|
|
||||||
<div class="event-indicator"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧分割线 -->
|
|
||||||
<div class="resize-handle-vertical" @mousedown="startResize('inspector', $event)">
|
|
||||||
<div class="resize-handle-hover"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧检查器面板 -->
|
|
||||||
<div class="inspector" :style="{ width: inspectorWidth + 'px' }">
|
|
||||||
<div class="inspector-content">
|
|
||||||
<div class="inspector-header">
|
|
||||||
<h3 class="inspector-title">
|
|
||||||
<span class="title-icon">🔍</span>
|
|
||||||
连接详情
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div 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">chrome.exe</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">PID</span>
|
|
||||||
<span class="detail-value">12345</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">协议</span>
|
|
||||||
<span class="detail-value">TCP</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">状态</span>
|
|
||||||
<span class="detail-value status-established">已建立</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">192.168.1.100:54321</div>
|
|
||||||
</div>
|
|
||||||
<div class="network-item">
|
|
||||||
<div class="network-label">远程地址</div>
|
|
||||||
<div class="network-value">142.250.191.14:443</div>
|
|
||||||
</div>
|
|
||||||
<div class="network-item">
|
|
||||||
<div class="network-label">域名</div>
|
|
||||||
<div class="network-value">google.com</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">1.2 MB</span>
|
|
||||||
</div>
|
|
||||||
<div class="traffic-item">
|
|
||||||
<span class="traffic-label">下载</span>
|
|
||||||
<span class="traffic-value download">5.8 MB</span>
|
|
||||||
</div>
|
|
||||||
<div class="traffic-item">
|
|
||||||
<span class="traffic-label">总计</span>
|
|
||||||
<span class="traffic-value total">7.0 MB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="traffic-chart">
|
|
||||||
<div class="chart-bars">
|
|
||||||
<div
|
|
||||||
v-for="i in 20"
|
|
||||||
:key="i"
|
|
||||||
class="chart-bar"
|
|
||||||
:style="{ height: Math.random() * 100 + '%' }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
|
||||||
import { NButton, NInput, NIcon, NRadioGroup, NRadioButton } from 'naive-ui'
|
|
||||||
import { SettingsOutline, MoonOutline, SearchOutline, SunnyOutline } from '@vicons/ionicons5'
|
|
||||||
import { useThemeStore } from '@/stores/theme'
|
|
||||||
|
|
||||||
const themeStore = useThemeStore()
|
|
||||||
const isDark = computed({
|
|
||||||
get: () => themeStore.isDark,
|
|
||||||
set: (val) => (themeStore.isDark = val),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 引用app容器元素
|
|
||||||
const appContainer = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const currentView = ref('monitor')
|
|
||||||
const searchQuery = ref('')
|
|
||||||
|
|
||||||
// 布局尺寸控制
|
|
||||||
const sidebarWidth = ref(300)
|
|
||||||
const inspectorWidth = ref(350)
|
|
||||||
const timelineHeight = ref(200)
|
|
||||||
const mainHeaderHeight = ref(80)
|
|
||||||
|
|
||||||
// 最小尺寸限制
|
|
||||||
const MIN_SIDEBAR_WIDTH = 250
|
|
||||||
const MAX_SIDEBAR_WIDTH = 450
|
|
||||||
const MIN_INSPECTOR_WIDTH = 300
|
|
||||||
const MAX_INSPECTOR_WIDTH = 500
|
|
||||||
const MIN_TIMELINE_HEIGHT = 150
|
|
||||||
const MAX_TIMELINE_HEIGHT = 350
|
|
||||||
|
|
||||||
// 视图选项
|
|
||||||
const viewOptions = [
|
|
||||||
{ label: '实时监控', value: 'monitor' },
|
|
||||||
{ label: '防火墙', value: 'firewall' },
|
|
||||||
{ label: '分析', value: 'analysis' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
const mockData = ref({
|
|
||||||
activeConnections: 247,
|
|
||||||
processCount: 43,
|
|
||||||
networkSpeed: '5.2 MB/s',
|
|
||||||
ruleCount: 156,
|
|
||||||
selectedConnection: 'chrome.exe → google.com',
|
|
||||||
connectionStatus: '已建立',
|
|
||||||
localIP: '192.168.1.100',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 拖拽状态
|
|
||||||
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 openSettings = () => {
|
|
||||||
console.log('打开设置')
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
|
||||||
isDark.value = !isDark.value
|
|
||||||
console.log('切换暗色模式:', isDark.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 键盘快捷键 - 重置布局
|
|
||||||
const resetLayout = () => {
|
|
||||||
sidebarWidth.value = 300
|
|
||||||
inspectorWidth.value = 350
|
|
||||||
timelineHeight.value = 200
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听键盘事件和主题切换
|
|
||||||
onMounted(() => {
|
|
||||||
// 获取app容器元素引用
|
|
||||||
appContainer.value = document.querySelector('.app-container')
|
|
||||||
|
|
||||||
// 初始化主题类
|
|
||||||
updateThemeClass()
|
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (event.ctrlKey && event.key === 'r') {
|
|
||||||
event.preventDefault()
|
|
||||||
resetLayout()
|
|
||||||
}
|
|
||||||
// 添加快捷键切换主题 Ctrl+T
|
|
||||||
if (event.ctrlKey && event.key === 't') {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleDarkMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeydown)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', handleKeydown)
|
|
||||||
document.removeEventListener('mousemove', handleResize)
|
|
||||||
document.removeEventListener('mouseup', stopResize)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听主题变化并更新CSS类
|
|
||||||
watch(isDark, () => {
|
|
||||||
nextTick(() => {
|
|
||||||
updateThemeClass()
|
|
||||||
})
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 更新主题CSS类
|
|
||||||
const updateThemeClass = () => {
|
|
||||||
if (appContainer.value) {
|
|
||||||
if (isDark.value) {
|
|
||||||
appContainer.value.classList.add('dark-theme')
|
|
||||||
} else {
|
|
||||||
appContainer.value.classList.remove('dark-theme')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 主题色彩变量 */
|
|
||||||
.app-container {
|
|
||||||
/* 暗色主题 */
|
|
||||||
--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;
|
|
||||||
|
|
||||||
--accent-primary: #3b82f6;
|
|
||||||
--accent-secondary: #06b6d4;
|
|
||||||
--accent-success: #22c55e;
|
|
||||||
--accent-warning: #f59e0b;
|
|
||||||
--accent-error: #ef4444;
|
|
||||||
--accent-purple: #8b5cf6;
|
|
||||||
|
|
||||||
--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);
|
|
||||||
|
|
||||||
--backdrop-blur: blur(20px);
|
|
||||||
--transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 亮色主题变量 */
|
|
||||||
.app-container:not(.dark-theme) {
|
|
||||||
--bg-primary: #ffffff;
|
|
||||||
--bg-secondary: #f8fafc;
|
|
||||||
--bg-tertiary: #f1f5f9;
|
|
||||||
--bg-quaternary: #e2e8f0;
|
|
||||||
--bg-overlay: rgba(255, 255, 255, 0.95);
|
|
||||||
--bg-glass: rgba(248, 250, 252, 0.8);
|
|
||||||
--bg-card: rgba(241, 245, 249, 0.8);
|
|
||||||
--bg-hover: rgba(59, 130, 246, 0.08);
|
|
||||||
--bg-selected: rgba(59, 130, 246, 0.15);
|
|
||||||
|
|
||||||
--text-primary: #0f172a;
|
|
||||||
--text-secondary: #1e293b;
|
|
||||||
--text-tertiary: #334155;
|
|
||||||
--text-quaternary: #475569;
|
|
||||||
--text-muted: #64748b;
|
|
||||||
--text-disabled: #94a3b8;
|
|
||||||
|
|
||||||
--border-primary: #e2e8f0;
|
|
||||||
--border-secondary: rgba(226, 232, 240, 0.8);
|
|
||||||
--border-tertiary: rgba(203, 213, 225, 0.6);
|
|
||||||
--border-hover: rgba(59, 130, 246, 0.3);
|
|
||||||
--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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 根据主题状态添加类 */
|
|
||||||
.app-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
|
||||||
color: var(--text-primary);
|
|
||||||
user-select: none;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 当暗色主题时添加 dark-theme 类 */
|
|
||||||
.app-container:global(.dark-theme) {
|
|
||||||
/* 暗色主题的额外样式 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部标题栏 */
|
|
||||||
.title-bar {
|
|
||||||
height: 60px;
|
|
||||||
border-bottom: 1px solid var(--border-primary);
|
|
||||||
background: var(--bg-overlay);
|
|
||||||
backdrop-filter: var(--backdrop-blur);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 24px;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
background: linear-gradient(135deg, var(--accent-primary) 0%, #1d4ed8 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon span {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 18px;
|
|
||||||
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-quaternary);
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: rgba(34, 197, 94, 0.1);
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent-success);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主内容区域 */
|
|
||||||
.main-content {
|
|
||||||
height: calc(100vh - 60px);
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 侧边栏 */
|
|
||||||
.sidebar {
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: var(--backdrop-blur);
|
|
||||||
border-right: 1px solid var(--border-primary);
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content {
|
|
||||||
padding: 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽分割线 */
|
|
||||||
.resize-handle-vertical {
|
|
||||||
width: 4px;
|
|
||||||
background: var(--border-tertiary);
|
|
||||||
cursor: col-resize;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle-vertical:hover {
|
|
||||||
background: var(--border-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle-hover {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0 -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle-horizontal {
|
|
||||||
height: 4px;
|
|
||||||
background: var(--border-tertiary);
|
|
||||||
cursor: row-resize;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle-horizontal:hover {
|
|
||||||
background: rgba(168, 85, 247, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle-hover-h {
|
|
||||||
position: absolute;
|
|
||||||
inset: -4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主视图 */
|
|
||||||
.main-view {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
background: var(--bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header {
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: var(--backdrop-blur);
|
|
||||||
border-bottom: 1px solid var(--border-primary);
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 连接区域 */
|
|
||||||
.connections-area {
|
|
||||||
flex: 1;
|
|
||||||
background: var(--bg-glass);
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-column {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-column:first-child {
|
|
||||||
flex: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-body {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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.established {
|
|
||||||
background: rgba(34, 197, 94, 0.2);
|
|
||||||
color: var(--accent-success);
|
|
||||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.traffic-info {
|
|
||||||
color: var(--accent-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 时间轴区域 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 12px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-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-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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-item.event-warning .event-indicator {
|
|
||||||
background: var(--accent-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-item.event-success .event-indicator {
|
|
||||||
background: var(--accent-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 检查器面板 */
|
|
||||||
.inspector {
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: var(--backdrop-blur);
|
|
||||||
border-left: 1px solid var(--border-primary);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inspector-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inspector-body {
|
|
||||||
flex: 1;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.traffic-chart {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid var(--border-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-bars {
|
|
||||||
display: flex;
|
|
||||||
align-items: end;
|
|
||||||
gap: 2px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-bar {
|
|
||||||
flex: 1;
|
|
||||||
background: linear-gradient(to top, var(--accent-primary), var(--accent-secondary));
|
|
||||||
border-radius: 2px;
|
|
||||||
min-height: 4px;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-bar:hover {
|
|
||||||
background: linear-gradient(to top, #1d4ed8, #0891b2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动条样式 */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border-tertiary);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--border-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画 */
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽时的视觉反馈 */
|
|
||||||
.resize-handle-vertical:active,
|
|
||||||
.resize-handle-horizontal:active {
|
|
||||||
background-color: var(--accent-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.search-area {
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保内容不会溢出 */
|
|
||||||
.overflow-hidden {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.min-w-0 {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 自定义Naive UI组件样式 */
|
|
||||||
:deep(.n-radio-group .n-radio-button) {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border-color: var(--border-tertiary);
|
|
||||||
color: var(--text-quaternary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-radio-group .n-radio-button:hover) {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
border-color: var(--border-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-radio-group .n-radio-button--checked) {
|
|
||||||
background: var(--bg-selected);
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
: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);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button) {
|
|
||||||
border-color: var(--border-tertiary);
|
|
||||||
color: var(--text-quaternary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button:hover) {
|
|
||||||
border-color: var(--border-hover);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button--primary-type) {
|
|
||||||
background: var(--accent-primary);
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button--primary-type:hover) {
|
|
||||||
background: #1d4ed8;
|
|
||||||
border-color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button--info-type) {
|
|
||||||
background: var(--accent-secondary);
|
|
||||||
border-color: var(--accent-secondary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button--info-type:hover) {
|
|
||||||
background: #0891b2;
|
|
||||||
border-color: #0891b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button--warning-type) {
|
|
||||||
background: var(--accent-warning);
|
|
||||||
border-color: var(--accent-warning);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button--warning-type:hover) {
|
|
||||||
background: #d97706;
|
|
||||||
border-color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主题切换过渡效果 */
|
|
||||||
* {
|
|
||||||
transition:
|
|
||||||
background-color 0.3s ease,
|
|
||||||
border-color 0.3s ease,
|
|
||||||
color 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动态主题类切换 */
|
|
||||||
.app-container {
|
|
||||||
&.dark-theme {
|
|
||||||
/* 暗色主题时的额外样式 */
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.dark-theme) {
|
|
||||||
/* 亮色主题时的额外样式 */
|
|
||||||
.chart-bar {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon {
|
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,1404 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="appContainer" class="app-container">
|
||||||
|
<!-- 顶部标题栏组件 -->
|
||||||
|
<TitleBar />
|
||||||
|
|
||||||
|
<!-- 主内容区域 - 可拖拽布局 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 左侧边栏 -->
|
||||||
|
<div class="sidebar" :style="{ width: sidebarWidth + 'px' }">
|
||||||
|
<div class="sidebar-content">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3 class="sidebar-title">系统概览</h3>
|
||||||
|
<div class="sidebar-badge">{{ mockData.processCount }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🔗</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ mockData.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">{{ mockData.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">{{ mockData.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">扫描威胁</div>
|
||||||
|
<div class="action-item">更新规则</div>
|
||||||
|
<div class="action-item">导出日志</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左侧分割线 -->
|
||||||
|
<div class="resize-handle-vertical" @mousedown="startResize('sidebar', $event)">
|
||||||
|
<div class="resize-handle-hover"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间主视图区域 -->
|
||||||
|
<div class="main-view">
|
||||||
|
<!-- 中间顶部 - 搜索和标题 -->
|
||||||
|
<div class="main-header" :style="{ height: mainHeaderHeight + '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
|
||||||
|
v-model:value="searchQuery"
|
||||||
|
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>过滤</n-button>
|
||||||
|
<n-button size="small" ghost>刷新</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间主体 - 连接列表表格 -->
|
||||||
|
<div class="connections-area">
|
||||||
|
<div class="connections-header">
|
||||||
|
<h3 class="connections-title">
|
||||||
|
<span class="title-icon">📊</span>
|
||||||
|
连接列表
|
||||||
|
<span class="connection-count">{{ mockData.activeConnections }} 个活跃连接</span>
|
||||||
|
</h3>
|
||||||
|
<div class="connections-controls">
|
||||||
|
<n-button size="small" type="info" ghost>暂停监控</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="connections-content">
|
||||||
|
<div class="table-container">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="table-column">进程</div>
|
||||||
|
<div class="table-column">本地地址</div>
|
||||||
|
<div class="table-column">远程地址</div>
|
||||||
|
<div class="table-column">状态</div>
|
||||||
|
<div class="table-column">流量</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-body">
|
||||||
|
<div
|
||||||
|
v-for="i in 15"
|
||||||
|
:key="i"
|
||||||
|
class="table-row"
|
||||||
|
:class="{ 'row-selected': i === 3 }"
|
||||||
|
>
|
||||||
|
<div class="table-cell">
|
||||||
|
<div class="process-info">
|
||||||
|
<div class="process-icon">🌐</div>
|
||||||
|
<span>chrome.exe</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-cell">192.168.1.{{ 100 + i }}</div>
|
||||||
|
<div class="table-cell">
|
||||||
|
{{ ['google.com', 'github.com', 'cloudflare.com'][i % 3] }}
|
||||||
|
</div>
|
||||||
|
<div class="table-cell">
|
||||||
|
<span class="status-badge established">已建立</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-cell">
|
||||||
|
<span class="traffic-info">{{ (Math.random() * 1000).toFixed(0) }} KB/s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间轴分割线 -->
|
||||||
|
<div class="resize-handle-horizontal" @mousedown="startResize('timeline', $event)">
|
||||||
|
<div class="resize-handle-hover-h"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间底部 - 时间轴事件流 -->
|
||||||
|
<div class="timeline-area" :style="{ height: timelineHeight + 'px' }">
|
||||||
|
<div class="timeline-header">
|
||||||
|
<h3 class="timeline-title">
|
||||||
|
<span class="title-icon">📈</span>
|
||||||
|
实时事件流
|
||||||
|
</h3>
|
||||||
|
<div class="timeline-controls">
|
||||||
|
<n-button size="small" type="warning" ghost>暂停</n-button>
|
||||||
|
<n-button size="small" ghost>清空</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="events-container">
|
||||||
|
<div
|
||||||
|
v-for="i in 12"
|
||||||
|
:key="i"
|
||||||
|
class="event-item"
|
||||||
|
:class="['event-' + ['info', 'warning', 'success'][i % 3]]"
|
||||||
|
>
|
||||||
|
<div class="event-time">{{ new Date().toLocaleTimeString() }}</div>
|
||||||
|
<div class="event-content">
|
||||||
|
<div class="event-type">{{ ['连接建立', '数据传输', '连接关闭'][i % 3] }}</div>
|
||||||
|
<div class="event-desc">chrome.exe → google.com:443</div>
|
||||||
|
</div>
|
||||||
|
<div class="event-indicator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧分割线 -->
|
||||||
|
<div class="resize-handle-vertical" @mousedown="startResize('inspector', $event)">
|
||||||
|
<div class="resize-handle-hover"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧检查器面板 -->
|
||||||
|
<div class="inspector" :style="{ width: inspectorWidth + 'px' }">
|
||||||
|
<div class="inspector-content">
|
||||||
|
<div class="inspector-header">
|
||||||
|
<h3 class="inspector-title">
|
||||||
|
<span class="title-icon">🔍</span>
|
||||||
|
连接详情
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div 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">chrome.exe</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">PID</span>
|
||||||
|
<span class="detail-value">12345</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">协议</span>
|
||||||
|
<span class="detail-value">TCP</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">状态</span>
|
||||||
|
<span class="detail-value status-established">已建立</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">192.168.1.100:54321</div>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<div class="network-label">远程地址</div>
|
||||||
|
<div class="network-value">142.250.191.14:443</div>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<div class="network-label">域名</div>
|
||||||
|
<div class="network-value">google.com</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">1.2 MB</span>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-item">
|
||||||
|
<span class="traffic-label">下载</span>
|
||||||
|
<span class="traffic-value download">5.8 MB</span>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-item">
|
||||||
|
<span class="traffic-label">总计</span>
|
||||||
|
<span class="traffic-value total">7.0 MB</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="traffic-chart">
|
||||||
|
<div class="chart-bars">
|
||||||
|
<div
|
||||||
|
v-for="i in 20"
|
||||||
|
:key="i"
|
||||||
|
class="chart-bar"
|
||||||
|
:style="{ height: Math.random() * 100 + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||||
|
import { NButton, NInput, NIcon } from 'naive-ui'
|
||||||
|
import { SearchOutline } from '@vicons/ionicons5'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import TitleBar from '@/components/TitleBar.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const isDark = computed({
|
||||||
|
get: () => themeStore.isDark,
|
||||||
|
set: (val) => (themeStore.isDark = val),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 引用app容器元素
|
||||||
|
const appContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const currentView = ref('monitor')
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
// 布局尺寸控制
|
||||||
|
const sidebarWidth = ref(300)
|
||||||
|
const inspectorWidth = ref(350)
|
||||||
|
const timelineHeight = ref(200)
|
||||||
|
const mainHeaderHeight = ref(80)
|
||||||
|
|
||||||
|
// 最小尺寸限制
|
||||||
|
const MIN_SIDEBAR_WIDTH = 250
|
||||||
|
const MAX_SIDEBAR_WIDTH = 450
|
||||||
|
const MIN_INSPECTOR_WIDTH = 300
|
||||||
|
const MAX_INSPECTOR_WIDTH = 500
|
||||||
|
const MIN_TIMELINE_HEIGHT = 150
|
||||||
|
const MAX_TIMELINE_HEIGHT = 350
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockData = ref({
|
||||||
|
activeConnections: 247,
|
||||||
|
processCount: 43,
|
||||||
|
networkSpeed: '5.2 MB/s',
|
||||||
|
ruleCount: 156,
|
||||||
|
selectedConnection: 'chrome.exe → google.com',
|
||||||
|
connectionStatus: '已建立',
|
||||||
|
localIP: '192.168.1.100',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
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 openSettings = () => {
|
||||||
|
console.log('打开设置')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
console.log('切换暗色模式:', isDark.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘快捷键 - 重置布局
|
||||||
|
const resetLayout = () => {
|
||||||
|
sidebarWidth.value = 300
|
||||||
|
inspectorWidth.value = 350
|
||||||
|
timelineHeight.value = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听键盘事件和主题切换
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取app容器元素引用
|
||||||
|
appContainer.value = document.querySelector('.app-container')
|
||||||
|
|
||||||
|
// 初始化主题类
|
||||||
|
updateThemeClass()
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听主题变化并更新CSS类
|
||||||
|
watch(isDark, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
updateThemeClass()
|
||||||
|
})
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 更新主题CSS类
|
||||||
|
const updateThemeClass = () => {
|
||||||
|
if (appContainer.value) {
|
||||||
|
if (isDark.value) {
|
||||||
|
appContainer.value.classList.add('dark-theme')
|
||||||
|
} else {
|
||||||
|
appContainer.value.classList.remove('dark-theme')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 主题色彩变量 */
|
||||||
|
.app-container {
|
||||||
|
/* 暗色主题 */
|
||||||
|
--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;
|
||||||
|
|
||||||
|
--accent-primary: #3b82f6;
|
||||||
|
--accent-secondary: #06b6d4;
|
||||||
|
--accent-success: #22c55e;
|
||||||
|
--accent-warning: #f59e0b;
|
||||||
|
--accent-error: #ef4444;
|
||||||
|
--accent-purple: #8b5cf6;
|
||||||
|
|
||||||
|
--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);
|
||||||
|
|
||||||
|
--backdrop-blur: blur(20px);
|
||||||
|
--transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 亮色主题变量 */
|
||||||
|
.app-container:not(.dark-theme) {
|
||||||
|
--bg-primary: #ffffff;
|
||||||
|
--bg-secondary: #f8fafc;
|
||||||
|
--bg-tertiary: #f1f5f9;
|
||||||
|
--bg-quaternary: #e2e8f0;
|
||||||
|
--bg-overlay: rgba(255, 255, 255, 0.95);
|
||||||
|
--bg-glass: rgba(248, 250, 252, 0.8);
|
||||||
|
--bg-card: rgba(241, 245, 249, 0.8);
|
||||||
|
--bg-hover: rgba(59, 130, 246, 0.08);
|
||||||
|
--bg-selected: rgba(59, 130, 246, 0.15);
|
||||||
|
|
||||||
|
--text-primary: #0f172a;
|
||||||
|
--text-secondary: #1e293b;
|
||||||
|
--text-tertiary: #334155;
|
||||||
|
--text-quaternary: #475569;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--text-disabled: #94a3b8;
|
||||||
|
|
||||||
|
--border-primary: #e2e8f0;
|
||||||
|
--border-secondary: rgba(226, 232, 240, 0.8);
|
||||||
|
--border-tertiary: rgba(203, 213, 225, 0.6);
|
||||||
|
--border-hover: rgba(59, 130, 246, 0.3);
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 根据主题状态添加类 */
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
|
color: var(--text-primary);
|
||||||
|
user-select: none;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当暗色主题时添加 dark-theme 类 */
|
||||||
|
.app-container:global(.dark-theme) {
|
||||||
|
/* 暗色主题的额外样式 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏 */
|
||||||
|
.sidebar {
|
||||||
|
background: var(--bg-glass);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border-right: 1px solid var(--border-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽分割线 */
|
||||||
|
.resize-handle-vertical {
|
||||||
|
width: 4px;
|
||||||
|
background: var(--border-tertiary);
|
||||||
|
cursor: col-resize;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-vertical:hover {
|
||||||
|
background: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-hover {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-horizontal {
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border-tertiary);
|
||||||
|
cursor: row-resize;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-horizontal:hover {
|
||||||
|
background: rgba(168, 85, 247, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-hover-h {
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主视图 */
|
||||||
|
.main-view {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header {
|
||||||
|
background: var(--bg-glass);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 连接区域 */
|
||||||
|
.connections-area {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-glass);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-column {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-column:first-child {
|
||||||
|
flex: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.established {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: var(--accent-success);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-info {
|
||||||
|
color: var(--accent-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间轴区域 */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-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-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item.event-warning .event-indicator {
|
||||||
|
background: var(--accent-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item.event-success .event-indicator {
|
||||||
|
background: var(--accent-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 检查器面板 */
|
||||||
|
.inspector {
|
||||||
|
background: var(--bg-glass);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border-left: 1px solid var(--border-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-chart {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--border-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 2px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bar {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(to top, var(--accent-primary), var(--accent-secondary));
|
||||||
|
border-radius: 2px;
|
||||||
|
min-height: 4px;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bar:hover {
|
||||||
|
background: linear-gradient(to top, #1d4ed8, #0891b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-tertiary);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽时的视觉反馈 */
|
||||||
|
.resize-handle-vertical:active,
|
||||||
|
.resize-handle-horizontal:active {
|
||||||
|
background-color: var(--accent-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.search-area {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保内容不会溢出 */
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-w-0 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义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);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button) {
|
||||||
|
border-color: var(--border-tertiary);
|
||||||
|
color: var(--text-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button:hover) {
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--primary-type) {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--primary-type:hover) {
|
||||||
|
background: #1d4ed8;
|
||||||
|
border-color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--info-type) {
|
||||||
|
background: var(--accent-secondary);
|
||||||
|
border-color: var(--accent-secondary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--info-type:hover) {
|
||||||
|
background: #0891b2;
|
||||||
|
border-color: #0891b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--warning-type) {
|
||||||
|
background: var(--accent-warning);
|
||||||
|
border-color: var(--accent-warning);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--warning-type:hover) {
|
||||||
|
background: #d97706;
|
||||||
|
border-color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题切换过渡效果 */
|
||||||
|
* {
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
border-color 0.3s ease,
|
||||||
|
color 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动态主题类切换 */
|
||||||
|
.app-container {
|
||||||
|
&.dark-theme {
|
||||||
|
/* 暗色主题时的额外样式 */
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.dark-theme) {
|
||||||
|
/* 亮色主题时的额外样式 */
|
||||||
|
.chart-bar {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
box-shadow: 0 0 8px rgba(34, 197, 94, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题切换过渡效果 - 只在页面加载完成后启用 */
|
||||||
|
body.theme-ready * {
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
border-color 0.3s ease,
|
||||||
|
color 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 初始加载时禁用所有过渡 */
|
||||||
|
body:not(.theme-ready) * {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对拖拽操作的过渡保持独立 */
|
||||||
|
.resize-handle-vertical,
|
||||||
|
.resize-handle-horizontal,
|
||||||
|
.table-row,
|
||||||
|
.stat-card,
|
||||||
|
.action-item,
|
||||||
|
.event-item {
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// vite-plugins/theme-inject.ts
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
export function themeInjectPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'theme-inject',
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
const themeScript = `
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const saved = localStorage.getItem('theme');
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const isDark = saved ? saved === 'dark' : prefersDark;
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#0c0c0c');
|
||||||
|
document.body.style.backgroundColor = '#0c0c0c';
|
||||||
|
document.body.style.color = '#ffffff';
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#ffffff');
|
||||||
|
document.body.style.backgroundColor = '#ffffff';
|
||||||
|
document.body.style.color = '#000000';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
return html.replace('<head>', `<head>${themeScript}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user