A:最后的调整
This commit is contained in:
1 parent
787b56633c
commit
77757bdadc
4 files changed
+289
-77
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user