U:调用真实UI

This commit is contained in:
jimmy committed 2025-08-10 14:50:59 +08:00
1 parent 3e7ca51ab0
commit 1732d279d9
3 files changed
+475 -205

No files matched your search

+1
View File
@@ -15,6 +15,7 @@ declare module 'vue' {
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default'] IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default'] IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default'] IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
NDataTable: typeof import('naive-ui')['NDataTable']
NetworkConnectionPanel2: typeof import('./src/components/NetworkConnectionPanel2.vue')['default'] NetworkConnectionPanel2: typeof import('./src/components/NetworkConnectionPanel2.vue')['default']
NHighlight: typeof import('naive-ui')['NHighlight'] NHighlight: typeof import('naive-ui')['NHighlight']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
@@ -92,7 +92,7 @@
v-model:value="selectedInterface" v-model:value="selectedInterface"
:options="interfaceOptions" :options="interfaceOptions"
size="small" size="small"
style="min-width: 120px;" :style="{ minWidth: interfaceSelectWidth }"
/> />
</div> </div>
</div> </div>
@@ -121,26 +121,14 @@
<!-- 软件流量分析区域 --> <!-- 软件流量分析区域 -->
<div class="software-analysis-area"> <div class="software-analysis-area">
<div class="software-ranking-panel"> <div class="software-ranking-panel" :class="{ 'large-screen': isLargeScreen }">
<div class="panel-header"> <div class="panel-header">
<h3 class="panel-title">软件流量TOP榜</h3> <h3 class="panel-title">软件流量TOP榜</h3>
<div class="panel-controls">
<n-button-group size="small">
<n-button
v-for="range in rankingTimeRanges"
:key="range"
:type="selectedRankingRange === range ? 'primary' : 'default'"
@click="selectedRankingRange = range"
>
{{ range }}
</n-button>
</n-button-group>
</div>
</div> </div>
<div class="ranking-content"> <div class="ranking-content">
<SoftwareRankingList <SoftwareRankingList
:data="softwareRankingData" :data="softwareRankingData"
:time-range="selectedRankingRange" :time-range="selectedTimeRange"
@select-software="showSoftwareDetail" @select-software="showSoftwareDetail"
/> />
</div> </div>
@@ -155,7 +143,7 @@
:network-relation-data="networkRelationData" :network-relation-data="networkRelationData"
:port-stats-data="portStatsData" :port-stats-data="portStatsData"
:protocol-data="protocolData" :protocol-data="protocolData"
:time-range="selectedRankingRange" :time-range="selectedTimeRange"
/> />
</div> </div>
</div> </div>
@@ -172,6 +160,8 @@ import {
AlertCircleOutline, AlertCircleOutline,
ServerOutline, ServerOutline,
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { httpClient } from '@/utils/http'
import type { ApiResponse } from '@/types/http'
// 导入组件 // 导入组件
import TrafficTrendChart from './components/TrafficTrendChart.vue' import TrafficTrendChart from './components/TrafficTrendChart.vue'
@@ -179,68 +169,255 @@ import TopAppsChart from './components/TopAppsChart.vue'
import SoftwareRankingList from './components/SoftwareRankingList.vue' import SoftwareRankingList from './components/SoftwareRankingList.vue'
import SoftwareDetailModal from './components/SoftwareDetailModal.vue' import SoftwareDetailModal from './components/SoftwareDetailModal.vue'
// API 接口类型定义
interface TimeRange {
type: string
name: string
available: boolean
startTime?: string
}
interface NetworkInterface {
id: string
name: string
displayName: string
isActive: boolean
macAddress: string
}
interface TrafficTrend {
interface: string
timeRange: string
points: {
timestamp: string
uploadSpeed: number
downloadSpeed: number
}[]
}
interface TopApp {
processName: string
displayName: string
icon: string
totalBytes: number
}
interface TopTrafficApp {
rank: number
processName: string
displayName: string
processPath: string
icon: string
version: string
company: string
totalBytes: number
uploadBytes: number
connectionCount: number
}
// 时间范围选项 // 时间范围选项
const timeRanges = ref([ const timeRanges = ref<TimeRange[]>([
{ type: '1hour', name: '1小时', available: true }, { type: '1hour', name: '1小时', available: true },
{ type: '1day', name: '24小时', available: true }, { type: '1day', name: '24小时', available: true },
{ type: '7days', name: '7天', available: true }, { type: '7days', name: '7天', available: true },
{ type: '30days', name: '30天', available: false }, { type: '30days', name: '30天', available: false },
]) ])
const selectedTimeRange = ref('1day') // API 调用函数
const getAvailableRanges = async () => {
try {
const res: ApiResponse<TimeRange[]> = await httpClient.get('/statistics/available-ranges')
if (res.success && res.data) {
// 根据 API 返回的数据更新时间范围选项
const ranges = Array.isArray(res.data) ? res.data : [res.data]
const rangeMap: Record<string, string> = {
'hour': '1hour',
'day': '1day',
'week': '7days',
'month': '30days'
}
// 更新时间范围的可用状态
timeRanges.value.forEach(range => {
const matchedRange = ranges.find(r => rangeMap[r.type] === range.type)
if (matchedRange) {
range.available = matchedRange.available
} else {
range.available = false
}
})
}
} catch (error) {
console.error('获取可用时间范围失败:', error)
}
}
const selectedTimeRange = ref('1hour') // 默认选择1小时,暂不支持10分钟选项
// 网络接口选项 // 网络接口选项
const interfaceOptions = ref([ const interfaceOptions = ref([
{ label: '全部网卡', value: 'all' }, { label: '全部网卡', value: 'all' },
{ label: '以太网', value: 'eth0' },
{ label: 'WiFi', value: 'wifi0' },
]) ])
// 计算网卡选择框的最小宽度
const interfaceSelectWidth = computed(() => {
if (interfaceOptions.value.length === 0) return '120px'
const maxLength = Math.max(
...interfaceOptions.value.map(option => option.label.length)
)
// 根据字符长度计算宽度,中文字符按2倍宽度计算
const estimatedWidth = maxLength * 8 + 60 // 基础60px + 字符宽度
return Math.max(estimatedWidth, 120) + 'px' // 最小120px
})
const getNetworkInterfaces = async () => {
try {
const params: Record<string, any> = {
timeRange: selectedTimeRange.value
}
const res: ApiResponse<NetworkInterface[]> = await httpClient.get('/statistics/interfaces', params)
if (res.success && res.data) {
// 清空现有选项(保留全部网卡选项)
interfaceOptions.value = [{ label: '全部网卡', value: 'all' }]
// 添加从 API 获取的网络接口
res.data.forEach(iface => {
if (iface.isActive) {
interfaceOptions.value.push({
label: iface.displayName,
value: iface.id
})
}
})
}
} catch (error) {
console.error('获取网络接口失败:', error)
}
}
const selectedInterface = ref('all') const selectedInterface = ref('all')
// 软件排行时间范围 // 大屏幕检测
const rankingTimeRanges = ref(['1小时', '1天', '7天', '30天']) const isLargeScreen = computed(() => {
const selectedRankingRange = ref('1天') if (typeof window === 'undefined') return false
return window.innerHeight >= 900 // 高度大于900px认为是大屏幕
})
// 移除单独的软件排行时间范围,使用顶部统一的时间选择
// 选中的软件和弹窗状态 // 选中的软件和弹窗状态
const selectedSoftware = ref<any>(null) const selectedSoftware = ref<any>(null)
const showDetailModal = ref(false) const showDetailModal = ref(false)
// Mock数据 // 流量趋势数据
const trafficTrendData = ref([ const trafficTrendData = ref<any[]>([])
{ timestamp: Date.now() - 3600000, uploadSpeed: 1024000, downloadSpeed: 5120000 },
{ timestamp: Date.now() - 2400000, uploadSpeed: 2048000, downloadSpeed: 8192000 },
{ timestamp: Date.now() - 1200000, uploadSpeed: 1536000, downloadSpeed: 6144000 },
{ timestamp: Date.now(), uploadSpeed: 3072000, downloadSpeed: 12288000 },
])
const topAppsData = ref([ const getTrafficTrends = async () => {
{ processName: 'chrome.exe', displayName: 'Google Chrome', totalBytes: 1073741824, percentage: 45.2 }, try {
{ processName: 'teams.exe', displayName: 'Microsoft Teams', totalBytes: 536870912, percentage: 22.5 }, const params: Record<string, any> = {
{ processName: 'spotify.exe', displayName: 'Spotify', totalBytes: 268435456, percentage: 11.3 }, timeRange: selectedTimeRange.value,
{ processName: 'code.exe', displayName: 'VS Code', totalBytes: 134217728, percentage: 5.6 }, interfaceId: selectedInterface.value
{ processName: 'slack.exe', displayName: 'Slack', totalBytes: 67108864, percentage: 2.8 }, }
])
const softwareRankingData = ref([ const res: ApiResponse<TrafficTrend> = await httpClient.get('/traffic/trends', params)
{ if (res.success && res.data) {
rank: 1, // 转换 API 数据为图表需要的格式并按时间排序(最新时间在右侧)
processName: 'chrome.exe', let processedData = res.data.points
displayName: 'Google Chrome', .map(point => ({
totalBytes: 1073741824, timestamp: parseInt(point.timestamp) * 1000, // 转换为毫秒时间戳
percentage: 45.2, uploadSpeed: point.uploadSpeed,
connectionCount: 23 downloadSpeed: point.downloadSpeed
}, }))
{ .sort((a, b) => a.timestamp - b.timestamp) // 按时间升序排列,最新时间在右侧
rank: 2,
processName: 'teams.exe', // 限制显示的数据点数量,避免图表过于密集
displayName: 'Microsoft Teams', const maxPoints = 250 // 最多显示250个点
totalBytes: 536870912, if (processedData.length > maxPoints) {
percentage: 22.5, // 等间隔采样数据点
connectionCount: 12 const step = Math.floor(processedData.length / maxPoints)
}, processedData = processedData.filter((_, index) => index % step === 0)
// ... 更多数据 }
])
trafficTrendData.value = processedData
}
} catch (error) {
console.error('获取流量趋势失败:', error)
}
}
// Top 应用流量数据
const topAppsData = ref<any[]>([])
const getTopApps = async () => {
try {
const params: Record<string, any> = {
timeRange: selectedTimeRange.value,
limit: 10
}
const res: ApiResponse<TopApp[]> = await httpClient.get('/traffic/top-apps', params)
if (res.success && res.data) {
// 计算总流量用于计算百分比
const totalBytes = res.data.reduce((sum, app) => sum + app.totalBytes, 0)
// 转换数据格式
topAppsData.value = res.data.map(app => ({
processName: app.processName,
displayName: app.displayName,
icon: app.icon,
totalBytes: app.totalBytes,
percentage: totalBytes > 0 ? (app.totalBytes / totalBytes * 100) : 0
}))
}
} catch (error) {
console.error('获取Top应用流量失败:', error)
}
}
// 软件流量排行数据
const softwareRankingData = ref<any[]>([])
const getSoftwareRanking = async () => {
try {
const params: Record<string, any> = {
timeRange: selectedTimeRange.value,
page: 1,
pageSize: 100
}
const res: ApiResponse<{
total: number
page: number
pageSize: number
items: TopTrafficApp[]
}> = await httpClient.get('/apps/top-traffic', params)
if (res.success && res.data) {
// 计算总流量用于计算百分比
const totalBytes = res.data.items.reduce((sum, app) => sum + app.totalBytes, 0)
// 转换数据格式适配组件
softwareRankingData.value = res.data.items.map(app => ({
rank: app.rank,
processName: app.processName,
displayName: app.displayName,
processPath: app.processPath,
icon: app.icon,
version: app.version,
company: app.company,
totalBytes: app.totalBytes,
uploadBytes: app.uploadBytes,
connectionCount: app.connectionCount,
percentage: totalBytes > 0 ? (app.totalBytes / totalBytes * 100) : 0
}))
}
} catch (error) {
console.error('获取软件流量排行失败:', error)
}
}
// 选中软件的详细信息 // 选中软件的详细信息
const selectedSoftwareInfo = computed(() => { const selectedSoftwareInfo = computed(() => {
@@ -295,22 +472,35 @@ const showSoftwareDetail = (software: any) => {
showDetailModal.value = true showDetailModal.value = true
} }
const refreshData = () => { const refreshData = async () => {
// 刷新数据的逻辑 // 刷新所有数据
console.log('Refreshing data...') console.log('Refreshing data...')
await Promise.all([
getAvailableRanges(),
getNetworkInterfaces(),
getTrafficTrends(),
getTopApps(),
getSoftwareRanking()
])
} }
// 监听时间范围变化 // 监听时间范围变化
watch(selectedTimeRange, () => { watch(selectedTimeRange, () => {
// 重新加载数据 // 重新加载相关数据
getTrafficTrends()
getTopApps()
getSoftwareRanking()
}) })
watch(selectedInterface, () => { watch(selectedInterface, () => {
// 重新加载趋势数据 // 重新加载趋势数据
getTrafficTrends()
}) })
onMounted(() => {
// 页面初始化,不再默认选择软件 onMounted(async () => {
// 页面初始化,加载所有数据
await refreshData()
}) })
</script> </script>
@@ -632,6 +822,11 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
} }
.software-ranking-panel.large-screen {
height: calc(100vh - 400px); /* 大屏幕下使用更多高度 */
min-height: 600px;
}
.panel-header { .panel-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid var(--border-primary); border-bottom: 1px solid var(--border-primary);
@@ -15,57 +15,34 @@
</div> </div>
</div> </div>
<div class="ranking-list" ref="listContainer"> <div class="table-container" ref="listContainer">
<div <n-data-table
v-for="(item, index) in filteredData" :columns="columns"
:key="item.processName" :data="filteredData"
:class="['ranking-item', { 'selected': selectedSoftware?.processName === item.processName }]" :row-key="(row: SoftwareRankingItem) => `${row.processName}_${row.rank}_${row.totalBytes}`"
@click="selectSoftware(item)" :row-class-name="(row: SoftwareRankingItem) => selectedSoftware?.processName === row.processName ? 'selected-row' : ''"
> :on-update:checked-row-keys="() => {}"
<div class="rank-badge" :class="getRankClass(item.rank)"> @row-click="selectSoftware"
{{ item.rank }} striped
</div> size="small"
:scroll-x="800"
<div class="software-icon"> :max-height="containerHeight"
<div class="icon-placeholder"> />
{{ item.displayName.charAt(0).toUpperCase() }}
</div>
</div>
<div class="software-info">
<div class="software-name">{{ item.displayName }}</div>
<div class="software-details">
<span class="process-name">{{ item.processName }}</span>
<span class="connection-count">{{ item.connectionCount }} 个连接</span>
</div>
</div>
<div class="traffic-info">
<div class="traffic-size">{{ formatBytes(item.totalBytes) }}</div>
<div class="traffic-percentage">{{ item.percentage.toFixed(1) }}%</div>
<div class="traffic-bar">
<div
class="traffic-fill"
:style="{ width: Math.min(item.percentage, 100) + '%' }"
></div>
</div>
</div>
</div>
<!-- 加载更多指示器 --> <!-- 数据统计信息 -->
<div v-if="hasMore" class="load-more" @click="loadMore"> <div class="data-info">
<n-button text size="small"> <span>共 {{ filteredData.length }} 条数据</span>
加载更多...
</n-button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick, h } from 'vue'
import { NInput, NIcon, NButton } from 'naive-ui' import { NInput, NIcon, NButton, NDataTable, NAvatar, NTag, NTooltip } from 'naive-ui'
import { SearchOutline } from '@vicons/ionicons5' import { SearchOutline } from '@vicons/ionicons5'
import type { DataTableColumns } from 'naive-ui'
import { getGradientColor } from '@/utils/colorUtils'
// 接口定义 // 接口定义
interface SoftwareRankingItem { interface SoftwareRankingItem {
@@ -73,6 +50,9 @@ interface SoftwareRankingItem {
processName: string processName: string
displayName: string displayName: string
processPath?: string processPath?: string
icon?: string
version?: string
company?: string
totalBytes: number totalBytes: number
uploadBytes?: number uploadBytes?: number
downloadBytes?: number downloadBytes?: number
@@ -95,8 +75,130 @@ const emit = defineEmits<{
// 响应式数据 // 响应式数据
const searchQuery = ref('') const searchQuery = ref('')
const listContainer = ref<HTMLElement>() const listContainer = ref<HTMLElement>()
const displayCount = ref(20) // 初始显示20个
const hasMore = computed(() => displayCount.value < filteredData.value.length) // 计算表格容器高度
const containerHeight = computed(() => {
if (typeof window === 'undefined') return '400px'
const windowHeight = window.innerHeight
// 大屏幕下使用更多高度,小屏幕下保持400px
if (windowHeight >= 900) {
return Math.max(windowHeight - 500, 400) + 'px'
}
return '400px'
})
// 表格列定义
const columns: DataTableColumns<SoftwareRankingItem> = [
{
title: '排名',
key: 'rank',
width: 60,
render: (row) => {
const rankClass = row.rank <= 3 ? 'top-three' : row.rank <= 10 ? 'top-ten' : 'normal'
return h('div', { class: `rank-badge ${rankClass}` }, row.rank.toString())
}
},
{
title: '图标',
key: 'icon',
width: 60,
render: (row) => {
return h('div', { class: 'icon-column' }, [
row.icon
? h('img', {
src: `data:image/png;base64,${row.icon}`,
class: 'app-icon',
alt: row.displayName
})
: h('div', {
class: 'app-icon-span',
style: getGradientColor(row.displayName.charAt(0).toUpperCase())
}, [
h('span', row.displayName.charAt(0).toUpperCase())
])
])
}
},
{
title: '软件名称',
key: 'displayName',
width: 150,
render: (row) => {
return h('div', { class: 'software-name-cell' }, row.displayName)
}
},
{
title: '公司',
key: 'company',
width: 120,
render: (row) => row.company || '-'
},
{
title: '版本',
key: 'version',
width: 100,
render: (row) => row.version || '-'
},
{
title: '路径',
key: 'processPath',
width: 200,
ellipsis: {
tooltip: true
},
render: (row) => row.processPath || '-'
},
{
title: '总流量',
key: 'totalBytes',
width: 100,
render: (row) => formatBytes(row.totalBytes),
sorter: {
compare: (a, b) => Number(a.totalBytes) - Number(b.totalBytes),
multiple: 1
}
},
{
title: '上传',
key: 'uploadBytes',
width: 100,
render: (row) => formatBytes(row.uploadBytes || 0),
sorter: {
compare: (a, b) => Number(a.uploadBytes || 0) - Number(b.uploadBytes || 0),
multiple: 1
}
},
{
title: '连接数',
key: 'connectionCount',
width: 80,
render: (row) => row.connectionCount.toString(),
sorter: {
compare: (a, b) => Number(a.connectionCount) - Number(b.connectionCount),
multiple: 1
}
},
{
title: '占比',
key: 'percentage',
width: 80,
render: (row) => {
return h('div', { class: 'percentage-cell' }, [
h('div', { class: 'percentage-text' }, `${row.percentage.toFixed(1)}%`),
h('div', { class: 'percentage-bar' }, [
h('div', {
class: 'percentage-fill',
style: { width: `${Math.min(row.percentage, 100)}%` }
})
])
])
},
sorter: {
compare: (a, b) => Number(a.percentage) - Number(b.percentage),
multiple: 1
}
}
]
// 搜索过滤 // 搜索过滤
const filteredData = computed(() => { const filteredData = computed(() => {
@@ -106,11 +208,13 @@ const filteredData = computed(() => {
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(item => filtered = filtered.filter(item =>
item.displayName.toLowerCase().includes(query) || item.displayName.toLowerCase().includes(query) ||
item.processName.toLowerCase().includes(query) item.processName.toLowerCase().includes(query) ||
(item.company && item.company.toLowerCase().includes(query))
) )
} }
return filtered.slice(0, displayCount.value) // 不再限制显示数量,显示所有数据
return filtered
}) })
// 选择软件 // 选择软件
@@ -118,11 +222,6 @@ const selectSoftware = (software: SoftwareRankingItem) => {
emit('selectSoftware', software) emit('selectSoftware', software)
} }
// 加载更多
const loadMore = () => {
displayCount.value += 20
}
// 获取排名样式类 // 获取排名样式类
const getRankClass = (rank: number): string => { const getRankClass = (rank: number): string => {
if (rank <= 3) return 'top-three' if (rank <= 3) return 'top-three'
@@ -141,20 +240,14 @@ const formatBytes = (bytes: number): string => {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}` return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
} }
// 重置显示数量当时间范围改变时 // 重置滚动位置当时间范围改变时
watch(() => props.timeRange, () => { watch(() => props.timeRange, () => {
displayCount.value = 20
nextTick(() => { nextTick(() => {
if (listContainer.value) { if (listContainer.value) {
listContainer.value.scrollTop = 0 listContainer.value.scrollTop = 0
} }
}) })
}) })
// 重置显示数量当搜索查询改变时
watch(searchQuery, () => {
displayCount.value = 20
})
</script> </script>
<style scoped> <style scoped>
@@ -174,42 +267,38 @@ watch(searchQuery, () => {
width: 100%; width: 100%;
} }
.ranking-list { .table-container {
flex: 1; flex: 1;
overflow-y: auto;
padding: 8px; padding: 8px;
} }
.ranking-item { .data-info {
display: flex; display: flex;
align-items: center; justify-content: center;
gap: 12px; padding: 8px;
padding: 12px; border-top: 1px solid var(--border-secondary);
border-radius: 8px; margin-top: 8px;
border: 1px solid transparent; font-size: 12px;
transition: all 0.2s ease; color: var(--text-muted);
cursor: pointer;
margin-bottom: 4px;
} }
.ranking-item:hover { /* 表格内容样式 */
background: var(--bg-hover); :deep(.n-data-table-td) {
border-color: var(--border-hover); padding: 8px 12px !important;
} }
.ranking-item.selected { :deep(.selected-row) {
background: var(--bg-selected); background-color: var(--bg-selected) !important;
border-color: var(--accent-primary);
} }
.rank-badge { .rank-badge {
width: 28px; width: 24px;
height: 28px; height: 24px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 12px; font-size: 11px;
font-weight: 700; font-weight: 700;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -217,7 +306,7 @@ watch(searchQuery, () => {
.rank-badge.top-three { .rank-badge.top-three {
background: linear-gradient(135deg, #ffd700, #ffed4e); background: linear-gradient(135deg, #ffd700, #ffed4e);
color: #92400e; color: #92400e;
box-shadow: 0 0 10px rgba(255, 215, 0, 0.3); box-shadow: 0 0 8px rgba(255, 215, 0, 0.3);
} }
.rank-badge.top-ten { .rank-badge.top-ten {
@@ -230,109 +319,94 @@ watch(searchQuery, () => {
color: var(--text-muted); color: var(--text-muted);
} }
.software-icon { .icon-column {
flex-shrink: 0; display: flex;
justify-content: center;
align-items: center;
} }
.icon-placeholder { .app-icon {
width: 36px; width: 28px;
height: 36px; height: 28px;
border-radius: 8px; border-radius: 6px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); object-fit: contain;
}
.app-icon-span {
width: 28px;
height: 28px;
background: linear-gradient(135deg, var(--accent-primary) 0%, #1d4ed8 100%);
border-radius: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.app-icon-span span {
font-size: 12px;
font-weight: 700;
color: white; color: white;
font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
font-size: 14px;
} }
.software-info { .software-name-cell {
flex: 1; font-size: 13px;
min-width: 0;
}
.software-name {
font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 4px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.software-details { .percentage-cell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
}
.percentage-text {
font-size: 11px; font-size: 11px;
color: var(--text-muted);
}
.process-name {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.traffic-info {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
min-width: 80px;
}
.traffic-size {
font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
font-family: 'JetBrains Mono', 'Fira Code', monospace; text-align: center;
} }
.traffic-percentage { .percentage-bar {
font-size: 11px; width: 40px;
color: var(--text-muted); height: 3px;
}
.traffic-bar {
width: 60px;
height: 4px;
background: var(--bg-tertiary); background: var(--bg-tertiary);
border-radius: 2px; border-radius: 2px;
overflow: hidden; overflow: hidden;
margin: 0 auto;
} }
.traffic-fill { .percentage-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
border-radius: 2px; border-radius: 2px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.load-more { /* 表格样式覆盖 */
display: flex; :deep(.n-data-table) {
justify-content: center; --n-border-color: var(--border-secondary);
padding: 16px; --n-th-color: var(--bg-tertiary);
border-top: 1px solid var(--border-secondary); --n-td-color: var(--bg-card);
margin-top: 8px; --n-td-color-hover: var(--bg-hover);
--n-text-color: var(--text-primary);
--n-th-text-color: var(--text-secondary);
} }
/* 滚动条样式 */ :deep(.n-data-table-thead) {
.ranking-list::-webkit-scrollbar { background: var(--bg-secondary);
width: 6px;
} }
.ranking-list::-webkit-scrollbar-track { :deep(.n-data-table-tr:hover) {
background: var(--bg-tertiary); cursor: pointer;
border-radius: 3px;
} }
.ranking-list::-webkit-scrollbar-thumb { :deep(.n-data-table-td) {
background: var(--border-secondary); border-bottom: 1px solid var(--border-tertiary) !important;
border-radius: 3px;
}
.ranking-list::-webkit-scrollbar-thumb:hover {
background: var(--border-hover);
} }
</style> </style>