A:最后的调整

This commit is contained in:
jimmy committed 2025-08-11 21:52:58 +08:00
1 parent 787b56633c
commit 77757bdadc
4 files changed
+289 -77

No files matched your search

-5
View File
@@ -73,8 +73,3 @@ if (bridge) {
// todo: 2025/6/25 08:32 开发阶段直接使用固定节点
webSocketStore.initialize('ws://127.0.0.1:8267')
}
// src\views\AnalyseView\AnalyseView.vue 中主要进行数据分析页面,而views\AnalyseView\components\SoftwareDetailModal.vue中可以获取到对应软件的AppId和时间信息,我们后端准备了
// /apps/network-analysis 接口,通过Get调用,这是我的测试用http://localhost:8268/api/apps/network-analysis?appId=0e1b50411d632b2c,目前没问题的,返回的信息格式
// 在C:\Users\charlatans\AppData\Roaming\JetBrains\WebStorm2025.1\scratches\scratch.json中有详细展示,你可以根据其定义内容以及你页面的展示方式,对于IP和端口信息我想使用
// Echarts中的graph-circular-layout来进行呈现。并且我的数据也很详细,针对软件自身的信息,以及网络信息可以有很多展示的内容,并且这个信息我更多的是希望用图标,比如Echarts来进行展示。
// 你帮我设计展示方式让其现代、优雅
@@ -141,6 +141,7 @@ const formatDuration = (firstSeen: string, lastSeen: string): string => {
.table-container {
height: 100%;
overflow-y: auto;
padding-bottom: 20px;
}
.stats-table {
@@ -1,16 +1,24 @@
<template>
<div class="network-analysis-chart">
<v-chart
<div class="network-analysis-chart" ref="containerRef">
<div v-if="!isContainerReady" class="loading-placeholder">
<n-spin size="large" />
<span>正在初始化图表...</span>
</div>
<v-chart
v-else
ref="chartRef"
class="chart"
:option="chartOption"
:autoresize="true"
:init-options="{ width: containerWidth, height: containerHeight }"
@finished="handleChartReady"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { NSpin } from 'naive-ui'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
@@ -89,6 +97,113 @@ const props = defineProps<{
loading?: boolean
}>()
// 响应式数据
const chartRef = ref()
const containerRef = ref<HTMLElement>()
const isContainerReady = ref(false)
const containerWidth = ref(400)
const containerHeight = ref(600)
// 检查容器尺寸
const checkContainerSize = async () => {
if (!containerRef.value) return false
await nextTick()
const rect = containerRef.value.getBoundingClientRect()
const width = rect.width || containerRef.value.clientWidth || containerRef.value.offsetWidth
const height = rect.height || containerRef.value.clientHeight || containerRef.value.offsetHeight
if (width > 0 && height > 0) {
containerWidth.value = width
containerHeight.value = height
return true
}
return false
}
// 等待容器准备就绪
const waitForContainer = async (maxAttempts = 10) => {
for (let i = 0; i < maxAttempts; i++) {
if (await checkContainerSize()) {
isContainerReady.value = true
return true
}
await new Promise(resolve => setTimeout(resolve, 100))
}
// 如果还是无法获取尺寸,使用默认值
console.warn('无法获取容器尺寸,使用默认值')
containerWidth.value = 600
containerHeight.value = 600
isContainerReady.value = true
return false
}
// ResizeObserver 监听容器尺寸变化
let resizeObserver: ResizeObserver | null = null
const setupResizeObserver = () => {
if (typeof ResizeObserver !== 'undefined' && containerRef.value) {
resizeObserver = new ResizeObserver(entries => {
const entry = entries[0]
if (entry && entry.contentRect) {
const { width, height } = entry.contentRect
if (width > 0 && height > 0) {
containerWidth.value = width
containerHeight.value = height
// 如果图表已经存在,刷新尺寸
if (chartRef.value && chartRef.value.chart) {
setTimeout(() => {
chartRef.value.chart.resize({
width: width,
height: height
})
}, 100)
}
}
}
})
resizeObserver.observe(containerRef.value)
}
}
// 图表就绪回调
const handleChartReady = () => {
console.log('图表渲染完成')
}
// 生命周期钩子
onMounted(async () => {
await waitForContainer()
setupResizeObserver()
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
})
// 暴露给父组件的方法
defineExpose({
chartRef,
resize: () => {
if (chartRef.value && chartRef.value.chart) {
checkContainerSize().then(() => {
chartRef.value.chart.resize({
width: containerWidth.value,
height: containerHeight.value
})
})
}
}
})
// 计算图表配置
const chartOption = computed<EChartsOption>(() => {
if (!props.data || props.loading) {
@@ -129,7 +244,12 @@ const chartOption = computed<EChartsOption>(() => {
formatter: '{b}',
fontSize: 12,
fontWeight: 'bold',
color: '#ffffff'
color: '#000000',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
padding: [4, 8],
borderRadius: 4,
borderColor: '#cccccc',
borderWidth: 1
}
})
nodeSet.add(appNodeId)
@@ -138,13 +258,13 @@ const chartOption = computed<EChartsOption>(() => {
topConnections.slice(0, 20).forEach((conn, index) => {
const isLocalhost = conn.remoteIP === '127.0.0.1'
const isMulticast = conn.remoteIP.startsWith('224.') || conn.remoteIP.startsWith('239.')
// 创建远程IP节点
const remoteIpNodeId = `ip_${conn.remoteIP}`
if (!nodeSet.has(remoteIpNodeId)) {
let ipCategory = 1 // 远程IP
let ipColor = '#10b981' // 绿色
if (isLocalhost) {
ipCategory = 3 // 本地回环
ipColor = '#f59e0b' // 橙色
@@ -152,7 +272,7 @@ const chartOption = computed<EChartsOption>(() => {
ipCategory = 4 // 组播地址
ipColor = '#8b5cf6' // 紫色
}
nodes.push({
id: remoteIpNodeId,
name: conn.remoteIP,
@@ -262,7 +382,7 @@ const chartOption = computed<EChartsOption>(() => {
case 3: typeText = '本地回环'; break
case 4: typeText = '组播地址'; break
}
return `
<div style="margin-bottom: 4px; font-weight: bold;">${node.name}</div>
<div>类型: ${typeText}</div>
@@ -335,11 +455,11 @@ const chartOption = computed<EChartsOption>(() => {
// 格式化字节数
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>
@@ -350,13 +470,27 @@ const formatBytes = (bytes: number): string => {
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
.chart {
min-height: 500px;
width: 100%;
height: 100%;
}
.loading-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 400px;
gap: 16px;
color: var(--text-muted);
font-size: 14px;
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
@@ -370,4 +504,4 @@ const formatBytes = (bytes: number): string => {
background: rgba(255, 255, 255, 0.8);
z-index: 10;
}
</style>
</style>
@@ -16,7 +16,9 @@
<h3 class="app-name">{{ analysisData?.appInfo.name || 'Unknown Application' }}</h3>
<p class="app-details">
{{ analysisData?.appInfo.company || 'Unknown Company' }}
<span v-if="analysisData?.appInfo.version">• v{{ analysisData.appInfo.version }}</span>
<span v-if="analysisData?.appInfo.version"
>• v{{ analysisData.appInfo.version }}</span
>
</p>
</div>
</div>
@@ -42,7 +44,9 @@
<div class="stat-label">总流量</div>
<div class="stat-trend">
<span class="upload">↑ {{ formatBytes(analysisData?.summary.totalUpload || 0) }}</span>
<span class="download">↓ {{ formatBytes(analysisData?.summary.totalDownload || 0) }}</span>
<span class="download"
>↓ {{ formatBytes(analysisData?.summary.totalDownload || 0) }}</span
>
</div>
</div>
@@ -67,11 +71,10 @@
<div class="modal-body">
<!-- 主要Tab内容区域 -->
<n-tabs
v-model:value="activeMainTab"
type="card"
size="large"
class="detail-tabs"
<n-tabs
v-model:value="activeMainTab"
type="card"
class="detail-tabs"
animated
:tab-style="{ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' }"
>
@@ -86,7 +89,9 @@
网络连接拓扑
</h4>
<div class="section-controls">
<span class="connection-count">{{ analysisData?.topConnections.length || 0 }} 个连接</span>
<span class="connection-count"
>{{ analysisData?.topConnections.length || 0 }} 个连接</span
>
<n-button size="small" @click="refreshData">
<template #icon>
<n-icon :component="RefreshOutline" />
@@ -126,10 +131,10 @@
</h4>
</div>
<div class="chart-container large">
<ProtocolChart
<ProtocolChart
ref="protocolChartRef"
v-if="protocolChartData.length > 0"
:data="protocolChartData"
v-if="protocolChartData.length > 0"
:data="protocolChartData"
/>
<div v-else class="empty-chart">
<n-icon :component="PieChartOutline" size="32" />
@@ -147,10 +152,10 @@
</h4>
</div>
<div class="chart-container large">
<TimeTrendChart
<TimeTrendChart
ref="trendChartRef"
v-if="timeTrendData.length > 0"
:data="timeTrendData"
v-if="timeTrendData.length > 0"
:data="timeTrendData"
/>
<div v-else class="empty-chart">
<n-icon :component="BarChartOutline" size="32" />
@@ -174,7 +179,9 @@
连接详情
</h4>
<div class="section-controls">
<span class="data-count">共 {{ analysisData?.topConnections.length || 0 }} 条记录</span>
<span class="data-count"
>共 {{ analysisData?.topConnections.length || 0 }} 条记录</span
>
<n-button size="small" @click="refreshData">
<template #icon>
<n-icon :component="RefreshOutline" />
@@ -263,7 +270,7 @@ import {
StatsChartOutline,
TrendingUpOutline,
PieChartOutline,
BarChartOutline
BarChartOutline,
} from '@vicons/ionicons5'
import { httpClient } from '@/utils/http'
import type { ApiResponse } from '@/types/http'
@@ -350,7 +357,7 @@ const emit = defineEmits<{
// 响应式数据
const showModal = computed({
get: () => props.show,
set: (value) => emit('update:show', value)
set: (value) => emit('update:show', value),
})
const analysisData = ref<NetworkAnalysisData | null>(null)
@@ -366,20 +373,65 @@ const trendChartRef = ref()
// 刷新所有图表尺寸
const resizeAllCharts = async () => {
await nextTick()
// 延迟执行,确保DOM已完全渲染
setTimeout(() => {
// 刷新网络图表
if (networkChartRef.value && networkChartRef.value.$refs?.chartRef?.chart) {
networkChartRef.value.$refs.chartRef.chart.resize()
try {
// 刷新网络图表 - 使用新的resize方法
if (networkChartRef.value && typeof networkChartRef.value.resize === 'function') {
networkChartRef.value.resize()
}
// 刷新协议图表
if (
protocolChartRef.value &&
protocolChartRef.value.chartRef &&
protocolChartRef.value.chartRef.chart
) {
const chart = protocolChartRef.value.chartRef.chart
if (chart && typeof chart.resize === 'function') {
chart.resize()
}
}
// 刷新趋势图表
if (
trendChartRef.value &&
trendChartRef.value.chartRef &&
trendChartRef.value.chartRef.chart
) {
const chart = trendChartRef.value.chartRef.chart
if (chart && typeof chart.resize === 'function') {
chart.resize()
}
}
} catch (error) {
console.warn('图表 resize 失败:', error)
}
// 刷新协议图表
if (protocolChartRef.value && protocolChartRef.value.$refs?.chartRef?.chart) {
protocolChartRef.value.$refs.chartRef.chart.resize()
}, 300)
// 再次延迟执行,确保图表完全就绪
setTimeout(() => {
try {
if (networkChartRef.value && typeof networkChartRef.value.resize === 'function') {
networkChartRef.value.resize()
}
if (
protocolChartRef.value &&
protocolChartRef.value.chartRef &&
protocolChartRef.value.chartRef.chart
) {
protocolChartRef.value.chartRef.chart.resize()
}
if (
trendChartRef.value &&
trendChartRef.value.chartRef &&
trendChartRef.value.chartRef.chart
) {
trendChartRef.value.chartRef.chart.resize()
}
} catch (error) {
console.warn('图表二次 resize 失败:', error)
}
// 刷新趋势图表
if (trendChartRef.value && trendChartRef.value.$refs?.chartRef?.chart) {
trendChartRef.value.$refs.chartRef.chart.resize()
}
}, 150)
}, 600)
}
// 监听Tab切换,刷新图表
@@ -388,13 +440,16 @@ watch(activeMainTab, () => {
})
// 监听弹窗显示状态
watch(() => props.show, (newShow) => {
if (newShow) {
nextTick(() => {
resizeAllCharts()
})
}
})
watch(
() => props.show,
(newShow) => {
if (newShow) {
nextTick(() => {
resizeAllCharts()
})
}
},
)
// 计算属性
const timeRangeText = computed(() => {
@@ -402,7 +457,7 @@ const timeRangeText = computed(() => {
'1hour': '1小时',
'1day': '24小时',
'7days': '7天',
'30days': '30天'
'30days': '30天',
}
return rangeMap[props.timeRange] || props.timeRange
})
@@ -415,29 +470,29 @@ const protocolChartData = computed(() => {
protocol: stat.protocol,
bytes: stat.totalTraffic,
percentage: stat.percentage,
color: colors[index % colors.length]
color: colors[index % colors.length],
}))
})
const timeTrendData = computed(() => {
if (!analysisData.value?.timeTrends) return []
return analysisData.value.timeTrends.map(trend => ({
return analysisData.value.timeTrends.map((trend) => ({
timestamp: trend.timestamp * 1000, // 转换为毫秒
timeStr: trend.timeStr,
upload: trend.upload,
download: trend.download,
connections: trend.connections
connections: trend.connections,
}))
})
const portStatsData = computed(() => {
if (!analysisData.value?.portAnalysis || !analysisData.value?.topConnections) return []
return analysisData.value.portAnalysis.map(port => {
return analysisData.value.portAnalysis.map((port) => {
// 从连接数据中获取使用该端口的远程主机
const connectionsForPort = analysisData.value!.topConnections.filter(
conn => conn.remotePort === port.port
(conn) => conn.remotePort === port.port,
)
return {
@@ -445,7 +500,7 @@ const portStatsData = computed(() => {
protocol: port.protocols[0] || 'TCP', // 取第一个协议
connectionCount: port.connectionCount,
totalBytes: port.totalTraffic,
remoteHosts: [...new Set(connectionsForPort.map(conn => conn.remoteIP))]
remoteHosts: [...new Set(connectionsForPort.map((conn) => conn.remoteIP))],
}
})
})
@@ -458,12 +513,12 @@ const fetchAnalysisData = async () => {
try {
const params = {
appId: props.appId,
timeRange: props.timeRange
timeRange: props.timeRange,
}
const response: ApiResponse<NetworkAnalysisData> = await httpClient.get(
'/apps/network-analysis',
params
params,
)
if (response.success && response.data) {
@@ -505,17 +560,24 @@ const formatBytes = (bytes: number): string => {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
watch(() => [props.appId, props.timeRange], () => {
if (props.show && props.appId) {
fetchAnalysisData()
}
}, { flush: 'post' })
watch(
() => [props.appId, props.timeRange],
() => {
if (props.show && props.appId) {
fetchAnalysisData()
}
},
{ flush: 'post' },
)
// 监听数据变化,刷新图表
watch(analysisData, () => {
resizeAllCharts()
}, { flush: 'post' })
watch(
analysisData,
() => {
resizeAllCharts()
},
{ flush: 'post' },
)
</script>
<style scoped>
@@ -526,15 +588,17 @@ watch(analysisData, () => {
}
.modal-card {
width: 1200px;
max-width: 90vw;
max-height: 90vh;
width: 1400px;
max-width: 95vw;
max-height: 95vh;
background: var(--bg-card);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* 弹窗头部 */
@@ -678,7 +742,14 @@ watch(analysisData, () => {
flex-shrink: 0;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-secondary);
padding: 8px 20px;
padding: 12px 20px;
position: relative;
z-index: 5;
min-height: 60px;
}
:deep(.detail-tabs .n-tabs-tab-wrapper) {
margin: 6px 4px;
}
:deep(.detail-tabs .n-tabs-tab) {
@@ -711,6 +782,13 @@ watch(analysisData, () => {
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
border-top: 3px solid var(--accent-primary);
border-left: 1px solid var(--accent-primary);
border-right: 1px solid var(--accent-primary);
border-bottom: none;
margin-bottom: -1px;
z-index: 10;
position: relative;
}
:deep(.detail-tabs .n-tabs-tab:hover:not(.n-tabs-tab--active)) {
@@ -834,6 +912,7 @@ watch(analysisData, () => {
.chart-section.full-height {
margin: 20px;
height: calc(100% - 40px);
min-height: 650px;
}
.table-section {
@@ -915,7 +994,7 @@ watch(analysisData, () => {
.chart-container.extra-large {
height: 100%;
min-height: 400px;
min-height: 600px;
}
.table-container {
@@ -1004,7 +1083,8 @@ watch(analysisData, () => {
font-size: 18px;
}
.app-icon, .app-icon-fallback {
.app-icon,
.app-icon-fallback {
width: 40px;
height: 40px;
}
@@ -1058,7 +1138,9 @@ watch(analysisData, () => {
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
.modal-card {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.4),
0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
}
</style>