A:剩余代码提交

This commit is contained in:
jimmy committed 2025-08-10 13:37:15 +08:00
1 parent 1d30e4fe77
commit 87f94a216b
9 files changed
+1152 -31

No files matched your search

@@ -51,7 +51,7 @@ public class NetworkDataCollectionService : IDisposable
// 每小时检测清理并合并旧数据
_dataCleanupTimer = new Timer(TriggerDataCleanupAndAggregation, null,
TimeSpan.FromHours(1), TimeSpan.FromHours(1));
TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
Log.Information("网络数据收集服务已启动");
}
@@ -378,4 +378,4 @@ public class NetworkCollectionStats
public DateTime LastNetworkInterfaceUpdate { get; set; }
public int CachedProcessCount { get; set; }
public int ActiveConnectionsCount { get; set; }
}
}
+13 -5
View File
@@ -1180,7 +1180,7 @@ public partial class App : System.Windows.Application
string displayName = app.AppName ?? "";
string iconBase64 = "";
if (string.IsNullOrEmpty(displayName) && !string.IsNullOrEmpty(app.AppId))
if (!string.IsNullOrEmpty(displayName) && !string.IsNullOrEmpty(app.AppId))
{
// 如果JOIN没有获取到应用名称,尝试单独查询
var appInfo = await DatabaseManager.Instance.AppInfos.GetAppByAppIdAsync(app.AppId);
@@ -1271,8 +1271,12 @@ public partial class App : System.Windows.Application
string processName = System.IO.Path.GetFileName(app.AppPath ?? "");
string displayName = app.AppName ?? "";
string iconBase64 = "";
if (string.IsNullOrEmpty(displayName) && !string.IsNullOrEmpty(app.AppId))
string version = "";
string company = "";
var b = !string.IsNullOrEmpty(app.AppId);
var isNullOrEmpty = !string.IsNullOrEmpty(displayName);
if (isNullOrEmpty && b)
{
// 如果JOIN没有获取到应用名称,尝试单独查询
var appInfo = await DatabaseManager.Instance.AppInfos.GetAppByAppIdAsync(app.AppId);
@@ -1280,10 +1284,12 @@ public partial class App : System.Windows.Application
{
displayName = appInfo.Name ?? "";
iconBase64 = appInfo.Base64Icon ?? "";
version = appInfo.Version ?? "";
company = appInfo.Company ?? "";
}
}
if (string.IsNullOrEmpty(displayName))
if (isNullOrEmpty)
{
displayName = processName.Replace(".exe", "") ?? "Unknown";
}
@@ -1295,6 +1301,8 @@ public partial class App : System.Windows.Application
displayName = displayName,
processPath = app.AppPath ?? "",
icon = iconBase64,
version = version,
company = company,
totalBytes = app.TotalTraffic,
uploadBytes = app.TotalUpload,
connectionCount = app.ConnectionCount
@@ -1757,4 +1765,4 @@ public class SubscriptionAppInfo : SubscriptionInfo
/// 订阅的应用程序路径
/// </summary>
public string ApplicationPath { get; set; }
}
}
+97
View File
@@ -0,0 +1,97 @@
### NetVizor API Test Cases
### Get network interfaces list - default 1 hour range
GET http://localhost:8268/api/statistics/interfaces
###
### Get network interfaces list - specify time range
GET http://localhost:8268/api/statistics/interfaces?timeRange=day
###
### Get network interfaces list - 1 week time range
GET http://localhost:8268/api/statistics/interfaces?timeRange=week
###
### Get network interfaces list - 1 month time range
GET http://localhost:8268/api/statistics/interfaces?timeRange=month
###
### Get available time ranges
GET http://localhost:8268/api/statistics/available-ranges
###
### Clear network interface cache
POST http://localhost:8268/api/statistics/clear-cache
Content-Type: application/json
{}
###
### Get traffic trends data - default 1 hour all interfaces
GET http://localhost:8268/api/traffic/trends
###
### Get traffic trends data - specify time range and interface
GET http://localhost:8268/api/traffic/trends?timeRange=1day&interfaceId=all
###
### Get traffic trends data - 1 week data
GET http://localhost:8268/api/traffic/trends?timeRange=7days&interfaceId=all
###
### Get traffic trends data - 1 month data
GET http://localhost:8268/api/traffic/trends?timeRange=30days&interfaceId=all
###
### Get top apps traffic data - default 1 hour top 10
GET http://localhost:8268/api/traffic/top-apps
###
### Get top apps traffic data - specify time range and limit
GET http://localhost:8268/api/traffic/top-apps?timeRange=1day&limit=20
###
### Get top apps traffic data - 1 week data
GET http://localhost:8268/api/traffic/top-apps?timeRange=7days&limit=15
###
### Get top apps traffic data - 1 month data
GET http://localhost:8268/api/traffic/top-apps?timeRange=30days&limit=50
###
### Get software traffic top ranking - default 1 hour page 1 100 items
GET http://localhost:8268/api/apps/top-traffic
###
### Get software traffic top ranking - specify time range and pagination
GET http://localhost:8268/api/apps/top-traffic?timeRange=1day&page=1&pageSize=50
###
### Get software traffic top ranking - page 2 data
GET http://localhost:8268/api/apps/top-traffic?timeRange=1day&page=2&pageSize=30
###
### Get software traffic top ranking - 1 week data
GET http://localhost:8268/api/apps/top-traffic?timeRange=7days&page=1&pageSize=100
###
### Get software traffic top ranking - 1 month data
GET http://localhost:8268/api/apps/top-traffic?timeRange=30days&page=1&pageSize=100
+3
View File
@@ -2,6 +2,9 @@
这是一个网络管理项目,它用的是Vue进行展示的,集成防火墙API,提供HTTP和WebSocket接口来实现数据的传递和接收。使用Vue作为前端并通过WebView2套壳实现主页面渲染
我使用了 TS 和Vue,UI样式使用的 Naive UI
\src\NetVizor.Frontend\.env.development 中有我前端的配置
vue-virtual-scroller 中是我项目主要使用的虚拟列表组件
我所有的环境并非全部在默认位置安装,比如部分环境如 Git 之类的在 D:\Env 下
+355
View File
@@ -0,0 +1,355 @@
# NetVizor Frontend - API 定义文档
## 1. 网络接口API
### 1.1 获取网络接口列表(在全局网速数据表中,把存在的网卡都展示出来,其中需要传递timeType,以表示拿哪里的数据,day指的是1天的数据,你应该去 GlobalNetworkHourly 中寻找,因为其内部最小单位为小时。如果是 week 则应该去 GlobalNetworkDaily 因为这个表的时间为天,mouth 也对应的是GlobalNetworkDaily,如果是 hour 则应该去 GlobalNetwork 找,他内部的时间是 5 秒。这个值一共4中可能,hour,day,week,mouth,默认 hour
目前数据库存储的有对应的 网卡ID,需要查询到所有的网卡ID再单独获取对应的网卡信息并返回 Common\Utils\NetworkAdapterHelper.cs 提供了网卡信息的获取方法
```
GET /api/statistics/interfaces?timeType=day
```
**返回:**
```json
{
"data": [
{
"id": "eth0",
"name": "以太网",
"displayName": "Realtek PCIe GbE Family Controller",
"isActive": true,
"macAddress": "00:11:22:33:44:55"
},
{
"id": "wifi0",
"name": "WiFi",
"displayName": "Intel Wi-Fi 6 AX200",
"isActive": false,
"macAddress": "66:77:88:99:AA:BB"
}
]
}
```
### 1.2 获取可用时间范围(这里的逻辑是是否有对应的数据,因为我们的数据库会定时的收集和整理信息,如果用户刚安装软件,肯定不会有月数据,也不会有周数据,所以需要据此来确定用户能展示的数据范围,同样分为 hour,day,week,mouth 返回这四个等级就行,hour只要用户安装超过10秒就有数据了,所以最低返回 hour 就行
```
GET /api/statistics/available-ranges
```
**返回:**
```json
{
"data": {
"type": "hour",
"name": "1小时",
"available": true,
"startTime": "2025-01-08T15:00:00Z"
}
}
```
## 2. 流量数据API
### 2.1 获取流量趋势数据
```
GET /api/traffic/trends
```
**参数:**
- `timeRange`: 时间范围 (1hour|1day|7days|30days)
- `interfaceId`: 网卡ID,可选,默认为"all"表示所有网卡
**返回:**
```json
{
"data": {
"interface": "all",
"timeRange": "1hour",
"points": [
{
"timestamp": "1754565364",
"uploadSpeed": 1024000,
"downloadSpeed": 5120000
},{
"timestamp": "1754565364",
"uploadSpeed": 1024000,
"downloadSpeed": 5120000
},{
"timestamp": "1754565364",
"uploadSpeed": 1024000,
"downloadSpeed": 5120000
},{
"timestamp": "1754565364",
"uploadSpeed": 1024000,
"downloadSpeed": 5120000
}
]
}
}
```
### 2.2 获取Top应用流量数据
```
GET /api/traffic/top-apps
```
**参数:**
- `timeRange`: 时间范围
- `limit`: 返回数量,默认10
**返回:**
```json
{
"data": [
{
"processName": "chrome.exe",
"displayName": "Google Chrome",
"icon": "+Uj0Iu09gH8tBUABUAB2HeA3IEEdYdHLY0EAAAAASUVORK5CYII=",
"totalBytes": 1073741824
},{
"processName": "chrome.exe",
"displayName": "Google Chrome",
"icon": "+Uj0Iu09gH8tBUABUAB2HeA3IEEdYdHLY0EAAAAASUVORK5CYII=",
"totalBytes": 1073741824
},{
"processName": "chrome.exe",
"displayName": "Google Chrome",
"icon": "+Uj0Iu09gH8tBUABUAB2HeA3IEEdYdHLY0EAAAAASUVORK5CYII=",
"totalBytes": 1073741824
}
]
}
```
## 3. 软件排行榜API
### 3.1 获取软件流量TOP100排行
```
GET /api/apps/top-traffic
```
**参数:**
- `timeRange`: 时间范围 (1hour|1day|7days|30days)
- `page`: 页码,默认1
- `pageSize`: 每页数量,默认100
**返回:**
```json
{
"data": {
"total": 156,
"page": 1,
"pageSize": 100,
"items": [
{
"rank": 1,
"processName": "chrome.exe",
"displayName": "Google Chrome",
"processPath": "C:\\Program Files\\Google\\Chrome\\chrome.exe",
"icon": "H8tBUABUAB2HeA3IEEdYdHLY0EAAAAASUVORK5CYII",
"totalBytes": 1073741824,
"uploadBytes": 104857600,
"connectionCount": 23
},{
"rank": 2,
"processName": "chrome.exe1",
"displayName": "Google Chrome1",
"processPath": "C:\\Program Files\\Google\\Chrome\\chrome.exe",
"icon": "H8tBUABUAB2HeA3IEEdYdHLY0EAAAAASUVORK5CYII",
"totalBytes": 1073741824,
"uploadBytes": 104857600,
"connectionCount": 32
}
]
}
}
```
## 4. 软件详情弹窗API(整合所有数据)
### 4.1 获取软件完整详情信息(新接口)
```
GET /api/apps/{processName}/complete-details
```
**参数:**
- `timeRange`: 时间范围
**返回(包含弹窗所需的所有数据):**
```json
{
"data": {
"basicInfo": {
"processName": "chrome.exe",
"displayName": "Google Chrome",
"version": "119.0.6045.160",
"company": "Google LLC",
"processPath": "C:\\Program Files\\Google\\Chrome\\chrome.exe",
"icon": "/path/to/icon.ico",
"fileSize": 2467840,
"createdTime": "2023-11-15T10:30:00Z",
"modifiedTime": "2023-11-15T10:30:00Z"
},
"trafficStats": {
"totalBytes": 1073741824,
"uploadBytes": 104857600,
"downloadBytes": 968884224,
"percentage": 45.2,
"connectionCount": 23,
"averageSpeed": 298666
},
"protocolDistribution": [
{
"protocol": "HTTPS",
"bytes": 1610612736,
"percentage": 37.5,
"color": "#3b82f6"
},
{
"protocol": "HTTP",
"bytes": 1073741824,
"percentage": 25.0,
"color": "#10b981"
},
{
"protocol": "DNS",
"bytes": 751619276,
"percentage": 17.5,
"color": "#f59e0b"
},
{
"protocol": "其他",
"bytes": 858993459,
"percentage": 20.0,
"color": "#ef4444"
}
],
"networkRelations": {
"nodes": [
{
"id": "app_chrome",
"name": "Chrome",
"type": "application",
"size": 45.2,
"category": 0
},
{
"id": "port_443",
"name": "443/TCP",
"type": "port",
"size": 30.5,
"category": 1
},
{
"id": "host_google",
"name": "google.com",
"type": "remote_host",
"size": 25.8,
"category": 2
}
],
"links": [
{
"source": "app_chrome",
"target": "port_443",
"value": 30.5,
"label": "30.5MB"
},
{
"source": "port_443",
"target": "host_google",
"value": 25.8,
"label": "25.8MB"
}
]
},
"portStats": [
{
"port": 443,
"protocol": "TCP",
"connectionCount": 15,
"totalBytes": 52428800,
"remoteHosts": [
"142.251.42.227",
"172.217.164.46"
]
},
{
"port": 80,
"protocol": "TCP",
"connectionCount": 5,
"totalBytes": 10485760,
"remoteHosts": [
"93.184.216.34"
]
}
],
"connections": [
{
"id": "conn_001",
"localAddress": "192.168.1.100",
"localPort": 52341,
"remoteAddress": "142.251.42.227",
"remotePort": 443,
"protocol": "TCP",
"state": "ESTABLISHED",
"uploadBytes": 2048000,
"downloadBytes": 8192000,
"duration": 1800,
"lastActivity": "2025-01-08T15:45:00Z"
}
]
}
}
```
## 5. 仪表盘指标API
### 5.1 获取关键指标数据
```
GET /api/dashboard/metrics
```
**参数:**
- `timeRange`: 时间范围
**返回:**
```json
{
"data": {
"totalTraffic": {
"value": "2.45 TB",
"trend": 12.5,
"trendDirection": "up"
},
"threatDetection": {
"value": 156,
"trend": -8.2,
"trendDirection": "down"
},
"averageLatency": {
"value": "12.3 ms",
"trend": 0.0,
"trendDirection": "stable"
},
"packetLoss": {
"value": "0.02%",
"trend": 0.01,
"trendDirection": "up"
}
}
}
```
## 说明
### 重要更新:
1. **整合弹窗API**: 新增 `/api/apps/{processName}/complete-details` 接口,一次性返回弹窗所需的所有数据(基本信息、协议分布、网络关系、端口统计等)
2. **简化交互**: 主页面只显示软件列表,详细信息通过弹窗展示
3. **数据优化**: 减少页面初始加载的数据量,按需加载详细信息
### Mock数据说明:
所有API接口在开发阶段使用Mock数据进行测试,生产环境对接真实的RESTful API。成功响应状态码200,失败时返回对应错误码和错误信息。
错误响应格式:
```json
{
"error": {
"code": "ERROR_CODE",
"message": "错误信息",
"details": {}
}
}
```
+89
View File
@@ -0,0 +1,89 @@
### NetVizor API Test Cases
### Get network interfaces list - default 1 hour range
GET http://localhost:8268/api/statistics/interfaces
###
### Get network interfaces list - specify time range
GET http://localhost:8268/api/statistics/interfaces?timeRange=day
###
### Get network interfaces list - 1 week time range
GET http://localhost:8268/api/statistics/interfaces?timeRange=week
###
### Get network interfaces list - 1 month time range
GET http://localhost:8268/api/statistics/interfaces?timeRange=month
###
### Get available time ranges
GET http://localhost:8268/api/statistics/available-ranges
###
### Get traffic trends data - default 1 hour all interfaces
GET http://localhost:8268/api/traffic/trends
###
### Get traffic trends data - specify time range and interface
GET http://localhost:8268/api/traffic/trends?timeRange=1day&interfaceId=all
###
### Get traffic trends data - 1 week data
GET http://localhost:8268/api/traffic/trends?timeRange=7days&interfaceId=all
###
### Get traffic trends data - 1 month data
GET http://localhost:8268/api/traffic/trends?timeRange=30days&interfaceId=all
###
### Get top apps traffic data - default 1 hour top 10
GET http://localhost:8268/api/traffic/top-apps
###
### Get top apps traffic data - specify time range and limit
GET http://localhost:8268/api/traffic/top-apps?timeRange=1day&limit=20
###
### Get top apps traffic data - 1 week data
GET http://localhost:8268/api/traffic/top-apps?timeRange=7days&limit=15
###
### Get top apps traffic data - 1 month data
GET http://localhost:8268/api/traffic/top-apps?timeRange=30days&limit=50
###
### Get software traffic top ranking - default 1 hour page 1 100 items
GET http://localhost:8268/api/apps/top-traffic
###
### Get software traffic top ranking - specify time range and pagination
GET http://localhost:8268/api/apps/top-traffic?timeRange=1day&page=1&pageSize=50
###
### Get software traffic top ranking - page 2 data
GET http://localhost:8268/api/apps/top-traffic?timeRange=1day&page=2&pageSize=30
###
### Get software traffic top ranking - 1 week data
GET http://localhost:8268/api/apps/top-traffic?timeRange=7days&page=1&pageSize=100
###
### Get software traffic top ranking - 1 month data
GET http://localhost:8268/api/apps/top-traffic?timeRange=30days&page=1&pageSize=100
@@ -10,8 +10,8 @@
<div class="header-controls">
<div class="time-selector">
<n-button-group>
<n-button
v-for="range in timeRanges"
<n-button
v-for="range in timeRanges"
:key="range.type"
:type="selectedTimeRange === range.type ? 'primary' : 'default'"
:disabled="!range.available"
@@ -31,7 +31,7 @@
</div>
<!-- 关键指标卡片 -->
<div class="metrics-grid">
<div class="metrics-grid" v-if="false ">
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">总流量</span>
@@ -88,8 +88,8 @@
<div class="chart-header">
<h3 class="chart-title">流量趋势</h3>
<div class="chart-controls">
<n-select
v-model:value="selectedInterface"
<n-select
v-model:value="selectedInterface"
:options="interfaceOptions"
size="small"
style="min-width: 120px;"
@@ -97,8 +97,8 @@
</div>
</div>
<div class="chart-body">
<TrafficTrendChart
:data="trafficTrendData"
<TrafficTrendChart
:data="trafficTrendData"
:interface-id="selectedInterface"
/>
</div>
@@ -111,7 +111,7 @@
<h3 class="chart-title">Top应用流量</h3>
</div>
<div class="chart-body">
<TopAppsChart
<TopAppsChart
:data="topAppsData"
:time-range="selectedTimeRange"
/>
@@ -126,8 +126,8 @@
<h3 class="panel-title">软件流量TOP榜</h3>
<div class="panel-controls">
<n-button-group size="small">
<n-button
v-for="range in rankingTimeRanges"
<n-button
v-for="range in rankingTimeRanges"
:key="range"
:type="selectedRankingRange === range ? 'primary' : 'default'"
@click="selectedRankingRange = range"
@@ -138,7 +138,7 @@
</div>
</div>
<div class="ranking-content">
<SoftwareRankingList
<SoftwareRankingList
:data="softwareRankingData"
:time-range="selectedRankingRange"
@select-software="showSoftwareDetail"
@@ -223,21 +223,21 @@ const topAppsData = ref([
])
const softwareRankingData = ref([
{
rank: 1,
processName: 'chrome.exe',
displayName: 'Google Chrome',
totalBytes: 1073741824,
{
rank: 1,
processName: 'chrome.exe',
displayName: 'Google Chrome',
totalBytes: 1073741824,
percentage: 45.2,
connectionCount: 23
connectionCount: 23
},
{
rank: 2,
processName: 'teams.exe',
displayName: 'Microsoft Teams',
totalBytes: 536870912,
{
rank: 2,
processName: 'teams.exe',
displayName: 'Microsoft Teams',
totalBytes: 536870912,
percentage: 22.5,
connectionCount: 12
connectionCount: 12
},
// ... 更多数据
])
@@ -668,7 +668,7 @@ onMounted(() => {
.chart-card.large {
grid-column: span 1;
}
.software-ranking-panel {
height: 450px;
}
@@ -0,0 +1,132 @@
<template>
<div class="protocol-chart">
<v-chart
ref="chartRef"
class="chart"
:option="chartOption"
:autoresize="true"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import type { EChartsOption } from 'echarts'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
// Props定义
interface ProtocolData {
protocol: string
bytes: number
percentage: number
color: string
}
const props = defineProps<{
data: ProtocolData[]
}>()
// 生成饼图配置
const chartOption = computed<EChartsOption>(() => {
return {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const data = params.data
return `
<div style="margin-bottom: 4px; font-weight: bold;">${data.name}</div>
<div>占比: ${data.value}%</div>
<div>流量: ${formatBytes(data.bytes)}</div>
`
},
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(255, 255, 255, 0.1)',
textStyle: {
color: '#ffffff',
fontSize: 12
}
},
legend: {
orient: 'vertical',
right: 10,
top: 20,
textStyle: {
color: 'var(--text-secondary)',
fontSize: 11
},
formatter: (name: string) => {
const item = props.data.find(d => d.protocol === name)
return `${name} ${item?.percentage.toFixed(1)}%`
}
},
series: [
{
name: '协议分布',
type: 'pie',
radius: ['40%', '70%'],
center: ['35%', '50%'],
data: props.data.map(item => ({
value: item.percentage,
name: item.protocol,
bytes: item.bytes,
itemStyle: {
color: item.color,
borderWidth: 2,
borderColor: '#fff'
}
})),
label: {
show: false
},
labelLine: {
show: false
},
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelayUpdate: (idx: number) => idx * 100
}
]
}
})
// 格式化字节数
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
</script>
<style scoped>
.protocol-chart {
width: 100%;
height: 200px;
min-height: 200px;
}
.chart {
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,437 @@
<template>
<n-modal
v-model:show="visible"
:mask-closable="false"
preset="card"
style="width: 90%; max-width: 1000px;"
:title="software?.displayName || '软件详情'"
size="huge"
:bordered="false"
:segmented="false"
>
<div v-if="software" class="software-detail-content">
<!-- 基本信息和统计 -->
<div class="detail-header">
<div class="software-icon">
<div class="icon-placeholder">
{{ software.displayName.charAt(0).toUpperCase() }}
</div>
</div>
<div class="software-summary">
<h2 class="software-name">{{ software.displayName }}</h2>
<div class="software-meta">
<span class="process-name">{{ software.processName }}</span>
<span class="traffic-info">{{ formatBytes(software.totalBytes) }} ({{ software.percentage.toFixed(1) }}%)</span>
<span class="connection-info">{{ software.connectionCount }} 个连接</span>
</div>
</div>
<div class="detail-stats">
<div class="stat-item">
<span class="stat-label">流量占比</span>
<span class="stat-value">{{ software.percentage.toFixed(1) }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">连接数</span>
<span class="stat-value">{{ software.connectionCount }}</span>
</div>
</div>
</div>
<!-- 选项卡内容 -->
<div class="detail-tabs">
<n-tabs default-value="overview" type="line">
<!-- 概览 -->
<n-tab-pane name="overview" tab="概览">
<div class="tab-content">
<div class="overview-grid">
<!-- 软件信息卡片 -->
<div class="info-section">
<h4 class="section-title">软件信息</h4>
<SoftwareInfoCard :software-info="softwareInfo" />
</div>
<!-- 协议分布图 -->
<div class="protocol-section">
<h4 class="section-title">协议分布</h4>
<ProtocolChart :data="protocolData" />
</div>
</div>
</div>
</n-tab-pane>
<!-- 网络连接 -->
<n-tab-pane name="network" tab="网络连接">
<div class="tab-content">
<div class="network-grid">
<!-- 连接关系图 -->
<div class="relation-section">
<h4 class="section-title">连接关系</h4>
<NetworkRelationChart
:data="networkRelationData"
:software="software"
/>
</div>
<!-- 端口统计表 -->
<div class="ports-section">
<h4 class="section-title">端口统计</h4>
<PortStatsTable :data="portStatsData" />
</div>
</div>
</div>
</n-tab-pane>
<!-- 流量详情 -->
<n-tab-pane name="traffic" tab="流量详情">
<div class="tab-content">
<div class="traffic-details">
<div class="traffic-chart-section">
<h4 class="section-title">流量趋势 ({{ timeRange }})</h4>
<div class="traffic-chart-placeholder">
<div class="placeholder-content">
<n-icon :component="TrendingUpOutline" size="48" />
<p>流量趋势图</p>
<p class="placeholder-desc">展示该软件的上传/下载流量变化</p>
</div>
</div>
</div>
<div class="traffic-stats-section">
<h4 class="section-title">流量统计</h4>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">总流量</div>
<div class="stat-value">{{ formatBytes(software.totalBytes || 0) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">上传</div>
<div class="stat-value">{{ formatBytes(software.uploadBytes || 0) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">下载</div>
<div class="stat-value">{{ formatBytes(software.downloadBytes || 0) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">平均速度</div>
<div class="stat-value">{{ formatSpeed(averageSpeed) }}</div>
</div>
</div>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
</div>
</div>
<template #action>
<div class="modal-actions">
<n-button @click="visible = false">关闭</n-button>
<n-button type="primary" @click="exportData">
<template #icon>
<n-icon :component="DownloadOutline" />
</template>
导出数据
</n-button>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { NModal, NTabs, NTabPane, NButton, NIcon } from 'naive-ui'
import { TrendingUpOutline, DownloadOutline } from '@vicons/ionicons5'
// 导入子组件
import SoftwareInfoCard from './SoftwareInfoCard.vue'
import NetworkRelationChart from './NetworkRelationChart.vue'
import PortStatsTable from './PortStatsTable.vue'
import ProtocolChart from './ProtocolChart.vue'
// Props定义
const props = defineProps<{
show: boolean
software?: any
softwareInfo?: any
networkRelationData?: any
portStatsData?: any[]
protocolData?: any[]
timeRange?: string
}>()
// Emits
const emit = defineEmits<{
'update:show': [value: boolean]
}>()
// 计算属性
const visible = computed({
get: () => props.show,
set: (value) => emit('update:show', value)
})
// 计算平均速度
const averageSpeed = computed(() => {
if (!props.software?.totalBytes) return 0
// 模拟计算,实际应根据时间范围计算
return props.software.totalBytes / 3600 // 假设1小时的数据
})
// 格式化字节数
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
// 格式化速度
const formatSpeed = (bytesPerSecond: number): string => {
return formatBytes(bytesPerSecond) + '/s'
}
// 导出数据
const exportData = () => {
// 实现数据导出功能
console.log('Export data for:', props.software?.processName)
}
</script>
<style scoped>
.software-detail-content {
max-height: 70vh;
overflow-y: auto;
}
/* 详情头部 */
.detail-header {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-secondary);
}
.software-icon .icon-placeholder {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 18px;
}
.software-summary {
flex: 1;
}
.software-name {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
}
.software-meta {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
}
.process-name {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--accent-primary);
font-weight: 500;
}
.traffic-info {
color: var(--text-secondary);
font-weight: 600;
}
.connection-info {
color: var(--text-muted);
}
.detail-stats {
display: flex;
gap: 24px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
}
/* 选项卡内容 */
.detail-tabs {
margin-top: 8px;
}
.tab-content {
padding: 16px 0;
}
/* 概览页面 */
.overview-grid {
display: grid;
grid-template-columns: 1fr 300px;
gap: 24px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
margin: 0 0 12px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.protocol-section {
display: flex;
flex-direction: column;
}
/* 网络连接页面 */
.network-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.relation-section {
display: flex;
flex-direction: column;
}
.ports-section {
display: flex;
flex-direction: column;
}
/* 流量详情页面 */
.traffic-details {
display: flex;
flex-direction: column;
gap: 24px;
}
.traffic-chart-placeholder {
height: 200px;
background: var(--bg-tertiary);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed var(--border-secondary);
}
.placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: var(--text-muted);
}
.placeholder-desc {
font-size: 12px;
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.stat-card {
background: var(--bg-tertiary);
padding: 16px;
border-radius: 8px;
border: 1px solid var(--border-secondary);
text-align: center;
}
.stat-card .stat-label {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-card .stat-value {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
/* 模态框操作 */
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* 响应式 */
@media (max-width: 768px) {
.detail-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.detail-stats {
justify-content: center;
}
.overview-grid,
.network-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Naive UI 覆盖样式 */
:deep(.n-tabs .n-tab-pane) {
padding: 0;
}
:deep(.n-tabs .n-tabs-nav) {
--n-tab-color-hover: var(--bg-hover);
--n-tab-text-color-active: var(--accent-primary);
--n-bar-color: var(--accent-primary);
}
</style>