A:主页面的整体搭建

This commit is contained in:
jimmy committed 2025-06-20 11:47:56 +08:00
1 parent ca6a579533
commit e55843eae2
6 files changed
+1160 -319

No files matched your search

+15 -8
View File
@@ -6,6 +6,8 @@ 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 } from 'vue'
import { NButton } from 'naive-ui' import { NButton } from 'naive-ui'
import { NConfigProvider, NGlobalStyle } from 'naive-ui'
import { useThemeStore } from '@/stores/theme'
const router = useRouter() const router = useRouter()
// 是否展示生成路由 // 是否展示生成路由
@@ -69,6 +71,10 @@ if (nodeEnv !== undefined && !StringUtils.isBlank(nodeEnv) && nodeEnv === app.DE
} }
// endregion // endregion
// 主题样式
const themeStore = useThemeStore()
const theme = themeStore.theme
// 拖拽相关函数 // 拖拽相关函数
function startDrag(event) { function startDrag(event) {
if (event.target.closest('.drag-handle')) { if (event.target.closest('.drag-handle')) {
@@ -116,7 +122,9 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="app-wrapper"> <n-config-provider class="app-wrapper" :theme="theme">
<n-global-style />
<!-- 可拖动的导航栏 --> <!-- 可拖动的导航栏 -->
<div <div
v-if="isShowGenerateRouter" v-if="isShowGenerateRouter"
@@ -129,11 +137,7 @@ onMounted(() => {
<span class="drag-icon">≡</span> <span class="drag-icon">≡</span>
</div> </div>
<ul class="route-list"> <ul class="route-list">
<li <li v-for="(subRoute, index) in subRouteLists" :key="index" class="route-item">
v-for="(subRoute, index) in subRouteLists"
:key="index"
class="route-item"
>
<n-button <n-button
:type="isActive(subRoute.path) ? 'primary' : 'default'" :type="isActive(subRoute.path) ? 'primary' : 'default'"
@click="listClickJump(subRoute)" @click="listClickJump(subRoute)"
@@ -148,7 +152,7 @@ onMounted(() => {
<div class="main-content"> <div class="main-content">
<router-view /> <router-view />
</div> </div>
</div> </n-config-provider>
</template> </template>
<style scoped> <style scoped>
@@ -163,7 +167,9 @@ onMounted(() => {
z-index: 9999; z-index: 9999;
background-color: white; background-color: white;
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
padding: 0.375rem; padding: 0.375rem;
min-width: 300px; min-width: 300px;
cursor: move; cursor: move;
@@ -195,6 +201,7 @@ onMounted(() => {
.route-item { .route-item {
text-align: center; text-align: center;
margin: 0.5rem; margin: 0.5rem;
background: green;
} }
.main-content { .main-content {
+10 -5
View File
@@ -8,6 +8,7 @@ import router from './router'
import CSharpBridgeV2 from '@/correspond/CSharpBridgeV2' 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'
const app = createApp(App) const app = createApp(App)
@@ -17,6 +18,9 @@ app.use(router)
app.mount('#app') app.mount('#app')
app.use(websocketPlugin) app.use(websocketPlugin)
// 使用 主题 支持
app.use(createNaiveUI())
// 注册 C# 函数 // 注册 C# 函数
window.externalFunctions = {} window.externalFunctions = {}
@@ -31,9 +35,10 @@ const intervalId = setInterval(() => {
clearInterval(intervalId) clearInterval(intervalId)
return return
} }
if (bridge){
bridge.send('GetWebSocketPath', {}, (data) => { bridge.send('GetWebSocketPath', {}, (data) => {
webSocketStore.initialize(data) webSocketStore.initialize(data)
console.log(data, '======') console.log(data, '======')
}) })
}
}, 500) }, 500)
+14
View File
@@ -0,0 +1,14 @@
// naive.ts
import {
create,
darkTheme,
lightTheme,
} from 'naive-ui'
let isDark = ref(false) // 用于控制主题状态,可接入 pinia 或 localStorage
export function createNaiveUI() {
return create({
theme: isDark.value ? darkTheme : lightTheme,
})
}
+30
View File
@@ -0,0 +1,30 @@
// stores/theme.ts
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
import { darkTheme, lightTheme } from 'naive-ui'
export const useThemeStore = defineStore('theme', () => {
// 初始读取系统设置或本地缓存
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
const saved = localStorage.getItem('theme')
const isDark = ref(saved ? saved === 'dark' : prefersDark.matches)
// 当前 Naive UI 的主题对象
const theme = computed(() => (isDark.value ? darkTheme : lightTheme))
// 切换主题
function toggleTheme() {
isDark.value = !isDark.value
}
// 自动保存主题状态
watch(isDark, (val) => {
localStorage.setItem('theme', val ? 'dark' : 'light')
})
return {
isDark,
theme,
toggleTheme,
}
})
+15 -11
View File
@@ -9,8 +9,8 @@ export class WebSocketManager {
private subscriptions: Map<string, { interval: number; timer?: NodeJS.Timeout }> = new Map() private subscriptions: Map<string, { interval: number; timer?: NodeJS.Timeout }> = new Map()
private messageQueue: WebSocketMessage[] = [] private messageQueue: WebSocketMessage[] = []
private state = WebSocketState.DISCONNECTED private state = WebSocketState.DISCONNECTED
private isReconnecting = false; private isReconnecting = false
private heartbeatTimer: number | null = null; private heartbeatTimer: number | null = null
public isInitialized: boolean = false public isInitialized: boolean = false
private heartbeatInterval = 30000 private heartbeatInterval = 30000
@@ -26,9 +26,11 @@ export class WebSocketManager {
get connectionState() { get connectionState() {
return this.state return this.state
} }
get reconnecting() { get reconnecting() {
return this.isReconnecting; return this.isReconnecting
} }
// 初始化WebSocket连接 // 初始化WebSocket连接
public initialize(url: string) { public initialize(url: string) {
console.log('initialize', url) console.log('initialize', url)
@@ -53,7 +55,7 @@ export class WebSocketManager {
// 重新激活订阅 // 重新激活订阅
this.reactivateSubscriptions() this.reactivateSubscriptions()
// 启动心跳 // 启动心跳
this.startHeartbeat(); this.startHeartbeat()
} }
this.socket.onmessage = (event) => { this.socket.onmessage = (event) => {
@@ -92,11 +94,13 @@ export class WebSocketManager {
if (isConnected1) return if (isConnected1) return
setTimeout(() => { setTimeout(() => {
if (!this.isManuallyClosed) { if (!this.isManuallyClosed) {
bridge.send('GetWebSocketPath', {}, (data) => { if (bridge) {
this.initialize(data) bridge.send('GetWebSocketPath', {}, (data) => {
console.log(data, '======') this.initialize(data)
this.retryConnect() console.log(data, '======')
}) this.retryConnect()
})
}
} }
}, delay) }, delay)
} }
@@ -105,9 +109,9 @@ export class WebSocketManager {
private startHeartbeat(): void { private startHeartbeat(): void {
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) { if (this.ws?.readyState === WebSocket.OPEN) {
this.send({ command: 'ping', data: null }); this.send({ command: 'ping', data: null })
} }
}, this.heartbeatInterval); }, this.heartbeatInterval)
} }
// 注册命令处理器 // 注册命令处理器
+1076 -295
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div ref="appContainer" class="app-container">
<!-- 顶部标题栏 --> <!-- 顶部标题栏 -->
<div class="title-bar"> <div class="title-bar">
<div class="title-bar-left"> <div class="title-bar-left">
@@ -12,11 +12,7 @@
<div class="view-switcher"> <div class="view-switcher">
<n-radio-group v-model:value="currentView" size="small"> <n-radio-group v-model:value="currentView" size="small">
<n-radio-button <n-radio-button v-for="option in viewOptions" :key="option.value" :value="option.value">
v-for="option in viewOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }} {{ option.label }}
</n-radio-button> </n-radio-button>
</n-radio-group> </n-radio-group>
@@ -25,27 +21,19 @@
<div class="title-bar-right"> <div class="title-bar-right">
<div class="status-indicator"> <div class="status-indicator">
<div class="status-dot"></div> <div class="status-dot active"></div>
<span>实时监控中</span> <span>实时监控中</span>
</div> </div>
<n-button <n-button circle size="small" quaternary @click="openSettings">
circle
size="small"
@click="openSettings"
>
<template #icon> <template #icon>
<n-icon :component="SettingsOutline" /> <n-icon :component="SettingsOutline" />
</template> </template>
</n-button> </n-button>
<n-button <n-button circle size="small" quaternary @click="toggleDarkMode">
circle
size="small"
@click="toggleDarkMode"
>
<template #icon> <template #icon>
<n-icon :component="MoonOutline" /> <n-icon :component="isDark ? SunnyOutline : MoonOutline" />
</template> </template>
</n-button> </n-button>
</div> </div>
@@ -54,118 +42,171 @@
<!-- 主内容区域 - 可拖拽布局 --> <!-- 主内容区域 - 可拖拽布局 -->
<div class="main-content"> <div class="main-content">
<!-- 左侧边栏 --> <!-- 左侧边栏 -->
<div <div class="sidebar" :style="{ width: sidebarWidth + 'px' }">
class="sidebar"
:style="{ width: sidebarWidth + 'px' }"
>
<div class="sidebar-content"> <div class="sidebar-content">
<h3 class="sidebar-title">侧边栏区域</h3> <div class="sidebar-header">
<div class="sidebar-items"> <h3 class="sidebar-title">系统概览</h3>
<div>• 系统概览组件</div> <div class="sidebar-badge">{{ mockData.processCount }}</div>
<div>• 进程列表组件</div>
<div>• 快速统计组件</div>
</div> </div>
<!-- 模拟数据展示 -->
<div class="sidebar-stats"> <div class="sidebar-stats">
<div class="stat-item">活跃连接: {{ mockData.activeConnections }}</div> <div class="stat-card">
<div class="stat-item">进程数: {{ mockData.processCount }}</div> <div class="stat-icon">🔗</div>
<div class="stat-item">网速: {{ mockData.networkSpeed }}</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>
</div> </div>
<!-- 左侧分割线 --> <!-- 左侧分割线 -->
<div <div class="resize-handle-vertical" @mousedown="startResize('sidebar', $event)">
class="resize-handle-vertical"
@mousedown="startResize('sidebar', $event)"
>
<div class="resize-handle-hover"></div> <div class="resize-handle-hover"></div>
</div> </div>
<!-- 中间主视图区域 --> <!-- 中间主视图区域 -->
<div class="main-view"> <div class="main-view">
<!-- 中间顶部 - 搜索和标题 --> <!-- 中间顶部 - 搜索和标题 -->
<div <div class="main-header" :style="{ height: mainHeaderHeight + 'px' }">
class="main-header" <div class="header-content">
:style="{ height: mainHeaderHeight + 'px' }" <div class="header-info">
> <h2 class="main-header-title">网络连接监控</h2>
<div> <p class="main-header-subtitle">实时监控系统网络活动</p>
<h2 class="main-header-title">主视图标题区域</h2> </div>
<p class="main-header-subtitle">搜索和控制组件</p>
</div>
<div class="search-area"> <div class="header-actions">
<n-input <div class="search-area">
v-model:value="searchQuery" <n-input
placeholder="搜索连接、域名或IP..." v-model:value="searchQuery"
size="small" placeholder="搜索连接、域名或IP..."
> size="medium"
<template #prefix> round
<n-icon :component="SearchOutline" /> >
</template> <template #prefix>
</n-input> <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> </div>
<!-- 中间主体 - 连接列表表格 --> <!-- 中间主体 - 连接列表表格 -->
<div class="connections-area"> <div class="connections-area">
<h3 class="connections-title">连接列表表格区域</h3> <div class="connections-header">
<div class="connections-content"> <h3 class="connections-title">
<div>• 连接状态表格组件</div> <span class="title-icon">📊</span>
<div>• 实时数据展示</div> 连接列表
<div>• 可选择和过滤</div> <span class="connection-count">{{ mockData.activeConnections }} 个活跃连接</span>
<div>• 响应式表格布局</div> </h3>
<div class="connections-controls">
<n-button size="small" type="info" ghost>暂停监控</n-button>
</div>
</div>
<!-- 模拟表格区域 --> <div class="connections-content">
<div class="mock-table"> <div class="table-container">
<div <div class="table-header">
v-for="i in 20" <div class="table-column">进程</div>
:key="i" <div class="table-column">本地地址</div>
class="mock-row" <div class="table-column">远程地址</div>
> <div class="table-column">状态</div>
<span>连接 {{ i }} - 模拟数据行 ({{ searchQuery || '无搜索' }})</span> <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> </div>
</div> </div>
<!-- 时间轴分割线 --> <!-- 时间轴分割线 -->
<div <div class="resize-handle-horizontal" @mousedown="startResize('timeline', $event)">
class="resize-handle-horizontal"
@mousedown="startResize('timeline', $event)"
>
<div class="resize-handle-hover-h"></div> <div class="resize-handle-hover-h"></div>
</div> </div>
<!-- 中间底部 - 时间轴事件流 --> <!-- 中间底部 - 时间轴事件流 -->
<div <div class="timeline-area" :style="{ height: timelineHeight + 'px' }">
class="timeline-area"
:style="{ height: timelineHeight + 'px' }"
>
<div class="timeline-header"> <div class="timeline-header">
<h3 class="timeline-title">时间轴事件流区域</h3> <h3 class="timeline-title">
<span class="title-icon">📈</span>
实时事件流
</h3>
<div class="timeline-controls"> <div class="timeline-controls">
<n-button size="small" type="primary">暂停</n-button> <n-button size="small" type="warning" ghost>暂停</n-button>
<n-button size="small">清空</n-button> <n-button size="small" ghost>清空</n-button>
</div> </div>
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-items"> <div class="events-container">
<div>• 实时事件组件</div> <div
<div>• 事件流展示</div> v-for="i in 12"
<div>• 时间轴控制</div> :key="i"
class="event-item"
<!-- 模拟事件列表 --> :class="['event-' + ['info', 'warning', 'success'][i % 3]]"
<div class="mock-events"> >
<div <div class="event-time">{{ new Date().toLocaleTimeString() }}</div>
v-for="i in 10" <div class="event-content">
:key="i" <div class="event-type">{{ ['连接建立', '数据传输', '连接关闭'][i % 3] }}</div>
class="mock-event" <div class="event-desc">chrome.exe → google.com:443</div>
>
<span>事件 {{ i }} - {{ new Date().toLocaleTimeString() }}</span>
</div> </div>
<div class="event-indicator"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -173,38 +214,108 @@
</div> </div>
<!-- 右侧分割线 --> <!-- 右侧分割线 -->
<div <div class="resize-handle-vertical" @mousedown="startResize('inspector', $event)">
class="resize-handle-vertical"
@mousedown="startResize('inspector', $event)"
>
<div class="resize-handle-hover"></div> <div class="resize-handle-hover"></div>
</div> </div>
<!-- 右侧检查器面板 --> <!-- 右侧检查器面板 -->
<div <div class="inspector" :style="{ width: inspectorWidth + 'px' }">
class="inspector"
:style="{ width: inspectorWidth + 'px' }"
>
<div class="inspector-content"> <div class="inspector-content">
<h3 class="inspector-title">连接详情检查器</h3> <div class="inspector-header">
<div class="inspector-items"> <h3 class="inspector-title">
<div>• 连接详情组件</div> <span class="title-icon">🔍</span>
<div>• 地理位置信息</div> 连接详情
<div>• 安全信息展示</div> </h3>
<div>• 流量统计图表</div>
</div> </div>
<!-- 模拟详情数据 --> <div class="inspector-body">
<div class="inspector-details"> <div class="detail-section">
<div class="detail-card"> <h4 class="detail-section-title">基本信息</h4>
<div class="detail-item">选中连接: {{ mockData.selectedConnection }}</div> <div class="detail-grid">
<div class="detail-item">状态: {{ mockData.connectionStatus }}</div> <div class="detail-item">
<div class="detail-item">宽度: {{ inspectorWidth }}px</div> <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>
<!-- 更多模拟内容 --> <div class="detail-section">
<div class="mock-content"> <h4 class="detail-section-title">网络信息</h4>
<div v-for="i in 15" :key="i" class="mock-bar"></div> <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>
@@ -214,34 +325,43 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import { NButton, NInput, NIcon, NRadioGroup, NRadioButton } from 'naive-ui' import { NButton, NInput, NIcon, NRadioGroup, NRadioButton } from 'naive-ui'
import { SettingsOutline, MoonOutline, SearchOutline } from '@vicons/ionicons5' 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 currentView = ref('monitor')
const searchQuery = ref('') const searchQuery = ref('')
const isDarkMode = ref(true)
// 布局尺寸控制 // 布局尺寸控制
const sidebarWidth = ref(280) const sidebarWidth = ref(300)
const inspectorWidth = ref(320) const inspectorWidth = ref(350)
const timelineHeight = ref(180) const timelineHeight = ref(200)
const mainHeaderHeight = ref(72) const mainHeaderHeight = ref(80)
// 最小尺寸限制 // 最小尺寸限制
const MIN_SIDEBAR_WIDTH = 200 const MIN_SIDEBAR_WIDTH = 250
const MAX_SIDEBAR_WIDTH = 500 const MAX_SIDEBAR_WIDTH = 450
const MIN_INSPECTOR_WIDTH = 250 const MIN_INSPECTOR_WIDTH = 300
const MAX_INSPECTOR_WIDTH = 600 const MAX_INSPECTOR_WIDTH = 500
const MIN_TIMELINE_HEIGHT = 120 const MIN_TIMELINE_HEIGHT = 150
const MAX_TIMELINE_HEIGHT = 400 const MAX_TIMELINE_HEIGHT = 350
// 视图选项 // 视图选项
const viewOptions = [ const viewOptions = [
{ label: '实时监控', value: 'monitor' }, { label: '实时监控', value: 'monitor' },
{ label: '防火墙', value: 'firewall' }, { label: '防火墙', value: 'firewall' },
{ label: '分析', value: 'analysis' } { label: '分析', value: 'analysis' },
] ]
// 模拟数据 // 模拟数据
@@ -252,7 +372,7 @@ const mockData = ref({
ruleCount: 156, ruleCount: 156,
selectedConnection: 'chrome.exe → google.com', selectedConnection: 'chrome.exe → google.com',
connectionStatus: '已建立', connectionStatus: '已建立',
localIP: '192.168.1.100' localIP: '192.168.1.100',
}) })
// 拖拽状态 // 拖拽状态
@@ -267,7 +387,7 @@ const resizing = ref<{
startX: 0, startX: 0,
startY: 0, startY: 0,
startWidth: 0, startWidth: 0,
startHeight: 0 startHeight: 0,
}) })
// 开始拖拽 // 开始拖拽
@@ -278,9 +398,9 @@ const startResize = (type: 'sidebar' | 'inspector' | 'timeline', event: MouseEve
type, type,
startX: event.clientX, startX: event.clientX,
startY: event.clientY, startY: event.clientY,
startWidth: type === 'sidebar' ? sidebarWidth.value : startWidth:
type === 'inspector' ? inspectorWidth.value : 0, type === 'sidebar' ? sidebarWidth.value : type === 'inspector' ? inspectorWidth.value : 0,
startHeight: type === 'timeline' ? timelineHeight.value : 0 startHeight: type === 'timeline' ? timelineHeight.value : 0,
} }
document.addEventListener('mousemove', handleResize) document.addEventListener('mousemove', handleResize)
@@ -300,12 +420,18 @@ const handleResize = (event: MouseEvent) => {
const newWidth = Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, startWidth + deltaX)) const newWidth = Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, startWidth + deltaX))
sidebarWidth.value = newWidth sidebarWidth.value = newWidth
} else if (type === 'inspector') { } else if (type === 'inspector') {
const deltaX = startX - event.clientX // 反向计算,因为右侧面板 const deltaX = startX - event.clientX
const newWidth = Math.min(MAX_INSPECTOR_WIDTH, Math.max(MIN_INSPECTOR_WIDTH, startWidth + deltaX)) const newWidth = Math.min(
MAX_INSPECTOR_WIDTH,
Math.max(MIN_INSPECTOR_WIDTH, startWidth + deltaX),
)
inspectorWidth.value = newWidth inspectorWidth.value = newWidth
} else if (type === 'timeline') { } else if (type === 'timeline') {
const deltaY = startY - event.clientY // 反向计算,因为底部面板 const deltaY = startY - event.clientY
const newHeight = Math.min(MAX_TIMELINE_HEIGHT, Math.max(MIN_TIMELINE_HEIGHT, startHeight + deltaY)) const newHeight = Math.min(
MAX_TIMELINE_HEIGHT,
Math.max(MIN_TIMELINE_HEIGHT, startHeight + deltaY),
)
timelineHeight.value = newHeight timelineHeight.value = newHeight
} }
} }
@@ -325,164 +451,361 @@ const openSettings = () => {
} }
const toggleDarkMode = () => { const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value isDark.value = !isDark.value
console.log('切换暗色模式:', isDarkMode.value) console.log('切换暗色模式:', isDark.value)
} }
// 键盘快捷键 - 重置布局 // 键盘快捷键 - 重置布局
const resetLayout = () => { const resetLayout = () => {
sidebarWidth.value = 280 sidebarWidth.value = 300
inspectorWidth.value = 320 inspectorWidth.value = 350
timelineHeight.value = 180 timelineHeight.value = 200
} }
// 监听键盘事件 // 监听键盘事件和主题切换
onMounted(() => { onMounted(() => {
// 获取app容器元素引用
appContainer.value = document.querySelector('.app-container')
// 初始化主题类
updateThemeClass()
const handleKeydown = (event: KeyboardEvent) => { const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === 'r') { if (event.ctrlKey && event.key === 'r') {
event.preventDefault() event.preventDefault()
resetLayout() resetLayout()
} }
// 添加快捷键切换主题 Ctrl+T
if (event.ctrlKey && event.key === 't') {
event.preventDefault()
toggleDarkMode()
}
} }
document.addEventListener('keydown', handleKeydown) document.addEventListener('keydown', handleKeydown)
// 清理函数
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown) document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('mousemove', handleResize) document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize) 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> </script>
<style scoped> <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 { .app-container {
min-height: 100vh; min-height: 100vh;
background-color: #111827; background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: white; color: var(--text-primary);
user-select: none; 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 { .title-bar {
height: 3rem; height: 60px;
border-bottom: 1px solid #374151; border-bottom: 1px solid var(--border-primary);
background-color: #1f2937; background: var(--bg-overlay);
backdrop-filter: var(--backdrop-blur);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 1rem; padding: 0 24px;
position: sticky;
top: 0;
z-index: 100;
} }
.title-bar-left { .title-bar-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 24px;
} }
.app-title { .app-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 12px;
} }
.app-icon { .app-icon {
width: 1.5rem; width: 32px;
height: 1.5rem; height: 32px;
background-color: #3b82f6; background: linear-gradient(135deg, var(--accent-primary) 0%, #1d4ed8 100%);
border-radius: 0.25rem; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
} }
.app-icon span { .app-icon span {
font-size: 0.75rem; font-size: 14px;
font-weight: 700;
color: white;
} }
.app-name { .app-name {
font-weight: 600; 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 { .title-bar-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 16px;
} }
.status-indicator { .status-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 8px;
font-size: 0.875rem; font-size: 14px;
color: #d1d5db; 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 { .status-dot {
width: 0.5rem; width: 8px;
height: 0.5rem; height: 8px;
background-color: #34d399;
border-radius: 50%; border-radius: 50%;
background: var(--accent-success);
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
/* 主内容区域 */ /* 主内容区域 */
.main-content { .main-content {
height: calc(100vh - 3rem); height: calc(100vh - 60px);
display: flex; display: flex;
} }
/* 侧边栏 */ /* 侧边栏 */
.sidebar { .sidebar {
background-color: #1f2937; background: var(--bg-glass);
border: 1px solid #3b82f6; backdrop-filter: var(--backdrop-blur);
border-right: 1px solid var(--border-primary);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sidebar-content { .sidebar-content {
padding: 1rem; padding: 24px;
overflow-y: auto; overflow-y: auto;
flex: 1; flex: 1;
} }
.sidebar-title { .sidebar-header {
font-size: 0.875rem; display: flex;
font-weight: 500; align-items: center;
color: #d1d5db; justify-content: space-between;
margin-bottom: 0.75rem; margin-bottom: 20px;
} }
.sidebar-items { .sidebar-title {
font-size: 0.75rem; font-size: 16px;
color: #9ca3af; font-weight: 600;
line-height: 1.5; 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 { .sidebar-stats {
margin-top: 1rem; display: flex;
padding: 0.75rem; flex-direction: column;
background-color: #374151; gap: 12px;
border-radius: 0.25rem; margin-bottom: 24px;
} }
.stat-item { .stat-card {
font-size: 0.75rem; background: var(--bg-card);
color: #d1d5db; border: 1px solid var(--border-tertiary);
margin-bottom: 0.25rem; border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
transition: var(--transition);
} }
.stat-item:last-child { .stat-card:hover {
margin-bottom: 0; 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 { .resize-handle-vertical {
width: 0.25rem; width: 4px;
background-color: #4b5563; background: var(--border-tertiary);
cursor: col-resize; cursor: col-resize;
transition: background-color 0.2s; transition: background-color 0.2s;
position: relative; position: relative;
@@ -490,17 +813,17 @@ onMounted(() => {
} }
.resize-handle-vertical:hover { .resize-handle-vertical:hover {
background-color: #60a5fa; background: var(--border-hover);
} }
.resize-handle-hover { .resize-handle-hover {
position: absolute; position: absolute;
inset: 0 -0.25rem; inset: 0 -4px;
} }
.resize-handle-horizontal { .resize-handle-horizontal {
height: 0.25rem; height: 4px;
background-color: #4b5563; background: var(--border-tertiary);
cursor: row-resize; cursor: row-resize;
transition: background-color 0.2s; transition: background-color 0.2s;
position: relative; position: relative;
@@ -508,12 +831,12 @@ onMounted(() => {
} }
.resize-handle-horizontal:hover { .resize-handle-horizontal:hover {
background-color: #a78bfa; background: rgba(168, 85, 247, 0.5);
} }
.resize-handle-hover-h { .resize-handle-hover-h {
position: absolute; position: absolute;
inset: -0.25rem 0; inset: -4px 0;
} }
/* 主视图 */ /* 主视图 */
@@ -522,183 +845,529 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: var(--bg-card);
} }
.main-header { .main-header {
background-color: #1f2937; background: var(--bg-glass);
border: 1px solid #10b981; backdrop-filter: var(--backdrop-blur);
border-bottom: 1px solid var(--border-primary);
padding: 0 24px;
}
.header-content {
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 1rem; }
.header-info {
flex: 1;
} }
.main-header-title { .main-header-title {
font-size: 1rem; font-size: 20px;
font-weight: 600; font-weight: 700;
color: var(--text-secondary);
margin: 0;
line-height: 1;
} }
.main-header-subtitle { .main-header-subtitle {
font-size: 0.75rem; font-size: 14px;
color: #9ca3af; color: var(--text-muted);
margin: 4px 0 0 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
} }
.search-area { .search-area {
width: 16rem; width: 300px;
}
.filter-buttons {
display: flex;
gap: 8px;
} }
/* 连接区域 */ /* 连接区域 */
.connections-area { .connections-area {
flex: 1; flex: 1;
background-color: #1f2937; background: var(--bg-glass);
border: 1px solid #eab308;
padding: 1rem;
overflow: hidden; 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 { .connections-title {
font-size: 0.875rem; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #d1d5db; color: var(--text-secondary);
margin-bottom: 0.75rem; 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 { .connections-content {
font-size: 0.75rem; flex: 1;
color: #9ca3af; overflow: hidden;
overflow-y: auto; padding: 0;
}
.table-container {
height: 100%; height: 100%;
display: flex;
flex-direction: column;
} }
.mock-table { .table-header {
margin-top: 1rem; 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;
} }
.mock-row { .table-column {
height: 2rem; flex: 1;
background-color: #374151; padding: 0 8px;
border-radius: 0.25rem; }
.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; display: flex;
align-items: center; align-items: center;
padding: 0 0.75rem; font-size: 13px;
margin-bottom: 0.5rem; color: var(--text-tertiary);
} }
.mock-row span { .table-cell:first-child {
font-size: 0.75rem; flex: 1.5;
color: #d1d5db; }
.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 { .timeline-area {
background-color: #1f2937; background: var(--bg-glass);
border: 1px solid #a855f7; backdrop-filter: var(--backdrop-blur);
padding: 1rem; border-top: 1px solid var(--border-primary);
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
} }
.timeline-header { .timeline-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-secondary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0.5rem; background: var(--bg-card);
} }
.timeline-title { .timeline-title {
font-size: 0.875rem; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #d1d5db; color: var(--text-secondary);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
} }
.timeline-controls { .timeline-controls {
display: flex; display: flex;
gap: 0.5rem; gap: 8px;
} }
.timeline-content { .timeline-content {
height: 100%; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 12px 24px;
} }
.timeline-items { .events-container {
font-size: 0.75rem; display: flex;
color: #9ca3af; flex-direction: column;
gap: 8px;
} }
.mock-events { .event-item {
margin-top: 0.5rem;
}
.mock-event {
height: 1.5rem;
background-color: #374151;
border-radius: 0.25rem;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 0.5rem; gap: 12px;
margin-bottom: 0.5rem; padding: 12px 16px;
background: var(--bg-card);
border-radius: 8px;
border-left: 3px solid transparent;
transition: var(--transition);
position: relative;
} }
.mock-event span { .event-item:hover {
font-size: 0.75rem; background: var(--bg-hover);
color: #d1d5db; }
.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 { .inspector {
background-color: #1f2937; background: var(--bg-glass);
border: 1px solid #ef4444; backdrop-filter: var(--backdrop-blur);
border-left: 1px solid var(--border-primary);
overflow: hidden; overflow: hidden;
} }
.inspector-content { .inspector-content {
padding: 1rem;
height: 100%; height: 100%;
overflow-y: auto; 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 { .inspector-title {
font-size: 0.875rem; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #d1d5db; color: var(--text-secondary);
margin-bottom: 0.75rem; margin: 0;
display: flex;
align-items: center;
gap: 8px;
} }
.inspector-items { .inspector-body {
font-size: 0.75rem; flex: 1;
color: #9ca3af; padding: 24px;
line-height: 1.5;
} }
.inspector-details { .detail-section {
margin-top: 1rem; margin-bottom: 24px;
} }
.detail-card { .detail-section:last-child {
padding: 0.75rem;
background-color: #374151;
border-radius: 0.25rem;
}
.detail-item {
font-size: 0.75rem;
color: #d1d5db;
margin-bottom: 0.25rem;
}
.detail-item:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.mock-content { .detail-section-title {
margin-top: 0.75rem; 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);
} }
.mock-bar { .detail-grid {
height: 1rem; display: grid;
background-color: #374151; grid-template-columns: 1fr 1fr;
border-radius: 0.25rem; gap: 12px 16px;
margin-bottom: 0.5rem; }
.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;
} }
/* 滚动条样式 */ /* 滚动条样式 */
@@ -712,36 +1381,42 @@ onMounted(() => {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.3); background: var(--border-tertiary);
border-radius: 3px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.5); background: var(--border-hover);
} }
/* 动画 */ /* 动画 */
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
opacity: 1; opacity: 1;
transform: scale(1);
} }
50% { 50% {
opacity: 0.5; opacity: 0.8;
transform: scale(1.1);
} }
} }
/* 拖拽时的视觉反馈 */ /* 拖拽时的视觉反馈 */
.resize-handle-vertical:active, .resize-handle-vertical:active,
.resize-handle-horizontal:active { .resize-handle-horizontal:active {
background-color: rgba(59, 130, 246, 0.8) !important; background-color: var(--accent-primary) !important;
} }
/* 防止文本选择 */ /* 响应式调整 */
.select-none { @media (max-width: 1200px) {
-webkit-user-select: none; .search-area {
-moz-user-select: none; width: 250px;
-ms-user-select: none; }
user-select: none;
.detail-grid {
grid-template-columns: 1fr;
}
} }
/* 确保内容不会溢出 */ /* 确保内容不会溢出 */
@@ -752,4 +1427,110 @@ onMounted(() => {
.min-w-0 { .min-w-0 {
min-width: 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> </style>