U:调用真实UI
This commit is contained in:
1 parent
1f62e9cf51
commit
e5cf954798
3 files changed
+935
-402
No files matched your search
@@ -138,11 +138,7 @@
|
|||||||
<!-- 软件详情弹窗 -->
|
<!-- 软件详情弹窗 -->
|
||||||
<SoftwareDetailModal
|
<SoftwareDetailModal
|
||||||
v-model:show="showDetailModal"
|
v-model:show="showDetailModal"
|
||||||
:software="selectedSoftware"
|
:app-id="selectedAppId"
|
||||||
:software-info="selectedSoftwareInfo"
|
|
||||||
:network-relation-data="networkRelationData"
|
|
||||||
:port-stats-data="portStatsData"
|
|
||||||
:protocol-data="protocolData"
|
|
||||||
:time-range="selectedTimeRange"
|
:time-range="selectedTimeRange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,6 +200,7 @@ interface TopApp {
|
|||||||
|
|
||||||
interface TopTrafficApp {
|
interface TopTrafficApp {
|
||||||
rank: number
|
rank: number
|
||||||
|
appId: string
|
||||||
processName: string
|
processName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
processPath: string
|
processPath: string
|
||||||
@@ -402,6 +399,7 @@ const getSoftwareRanking = async () => {
|
|||||||
// 转换数据格式适配组件
|
// 转换数据格式适配组件
|
||||||
softwareRankingData.value = res.data.items.map(app => ({
|
softwareRankingData.value = res.data.items.map(app => ({
|
||||||
rank: app.rank,
|
rank: app.rank,
|
||||||
|
appId: app.appId,
|
||||||
processName: app.processName,
|
processName: app.processName,
|
||||||
displayName: app.displayName,
|
displayName: app.displayName,
|
||||||
processPath: app.processPath,
|
processPath: app.processPath,
|
||||||
@@ -419,56 +417,14 @@ const getSoftwareRanking = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选中软件的详细信息
|
|
||||||
const selectedSoftwareInfo = computed(() => {
|
|
||||||
if (!selectedSoftware.value) return null
|
|
||||||
return {
|
|
||||||
processName: 'chrome.exe',
|
|
||||||
displayName: 'Google Chrome',
|
|
||||||
version: '119.0.6045.160',
|
|
||||||
company: 'Google LLC',
|
|
||||||
processPath: 'C:\\Program Files\\Google\\Chrome\\chrome.exe',
|
|
||||||
fileSize: 2467840,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const networkRelationData = computed(() => {
|
// 选中的AppId
|
||||||
if (!selectedSoftware.value) return { nodes: [], links: [] }
|
const selectedAppId = ref<string | null>(null)
|
||||||
return {
|
|
||||||
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' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const portStatsData = computed(() => {
|
|
||||||
if (!selectedSoftware.value) return []
|
|
||||||
return [
|
|
||||||
{ 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'] },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 协议分布数据(移到弹窗中使用)
|
|
||||||
const protocolData = computed(() => {
|
|
||||||
if (!selectedSoftware.value) return []
|
|
||||||
return [
|
|
||||||
{ 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' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const showSoftwareDetail = (software: any) => {
|
const showSoftwareDetail = (software: any) => {
|
||||||
selectedSoftware.value = software
|
selectedAppId.value = software.appId
|
||||||
|
selectedSoftware.value = software // 保留用于兼容性
|
||||||
showDetailModal.value = true
|
showDetailModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,147 +1,161 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="visible"
|
v-model:show="visible"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
preset="card"
|
preset="card"
|
||||||
style="width: 90%; max-width: 1000px;"
|
style="width: 90%; max-width: 1000px"
|
||||||
:title="software?.displayName || '软件详情'"
|
:title="softwareDetail?.displayName || '软件详情'"
|
||||||
size="huge"
|
size="huge"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
:segmented="false"
|
:segmented="false"
|
||||||
>
|
>
|
||||||
<div v-if="software" class="software-detail-content">
|
<n-spin :show="loading" style="min-height: 400px">
|
||||||
<!-- 基本信息和统计 -->
|
<div v-if="softwareDetail" class="software-detail-content">
|
||||||
<div class="detail-header">
|
<!-- 基本信息和统计 -->
|
||||||
<div class="software-icon">
|
<div class="detail-header">
|
||||||
<div class="icon-placeholder">
|
<div class="software-icon">
|
||||||
{{ software.displayName.charAt(0).toUpperCase() }}
|
<img
|
||||||
|
v-if="softwareDetail.icon"
|
||||||
|
:src="`data:image/png;base64,${softwareDetail.icon}`"
|
||||||
|
class="app-icon"
|
||||||
|
:alt="softwareDetail.displayName"
|
||||||
|
/>
|
||||||
|
<div v-else class="icon-placeholder">
|
||||||
|
{{ softwareDetail.displayName.charAt(0).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="software-summary">
|
||||||
|
<h2 class="software-name">{{ softwareDetail.displayName }}</h2>
|
||||||
|
<div class="software-meta">
|
||||||
|
<span class="process-name">{{ softwareDetail.processName }}</span>
|
||||||
|
<span class="traffic-info"
|
||||||
|
>{{ formatBytes(softwareDetail.totalBytes) }} ({{
|
||||||
|
softwareDetail.percentage.toFixed(1)
|
||||||
|
}}%)</span
|
||||||
|
>
|
||||||
|
<span class="connection-info">{{ softwareDetail.connectionCount }} 个连接</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">流量占比</span>
|
||||||
|
<span class="stat-value">{{ softwareDetail.percentage.toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">连接数</span>
|
||||||
|
<span class="stat-value">{{ softwareDetail.connectionCount }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="software-summary">
|
|
||||||
<h2 class="software-name">{{ software.displayName }}</h2>
|
<!-- 选项卡内容 -->
|
||||||
<div class="software-meta">
|
<div class="detail-tabs">
|
||||||
<span class="process-name">{{ software.processName }}</span>
|
<n-tabs default-value="overview" type="line">
|
||||||
<span class="traffic-info">{{ formatBytes(software.totalBytes) }} ({{ software.percentage.toFixed(1) }}%)</span>
|
<!-- 概览 -->
|
||||||
<span class="connection-info">{{ software.connectionCount }} 个连接</span>
|
<n-tab-pane name="overview" tab="概览">
|
||||||
</div>
|
<div class="tab-content">
|
||||||
</div>
|
<div class="overview-grid">
|
||||||
<div class="detail-stats">
|
<!-- 软件信息卡片 -->
|
||||||
<div class="stat-item">
|
<div class="info-section">
|
||||||
<span class="stat-label">流量占比</span>
|
<h4 class="section-title">软件信息</h4>
|
||||||
<span class="stat-value">{{ software.percentage.toFixed(1) }}%</span>
|
<SoftwareInfoCard :software-info="softwareInfo" />
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">连接数</span>
|
<!-- 协议分布图 -->
|
||||||
<span class="stat-value">{{ software.connectionCount }}</span>
|
<div class="protocol-section">
|
||||||
</div>
|
<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="softwareDetail" />
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选项卡内容 -->
|
<div v-else-if="!loading && !softwareDetail" class="no-data">
|
||||||
<div class="detail-tabs">
|
<div class="no-data-icon">📊</div>
|
||||||
<n-tabs default-value="overview" type="line">
|
<div class="no-data-text">暂无软件详情数据</div>
|
||||||
<!-- 概览 -->
|
|
||||||
<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>
|
||||||
</div>
|
</n-spin>
|
||||||
|
|
||||||
<template #action>
|
<template #action>
|
||||||
<div class="modal-actions">
|
<n-button @click="visible = false">关闭</n-button>
|
||||||
<n-button @click="visible = false">关闭</n-button>
|
<n-button type="primary" @click="fetchAllData" :loading="loading">
|
||||||
<n-button type="primary" @click="exportData">
|
<template #icon>
|
||||||
<template #icon>
|
<n-icon :component="RefreshOutline" />
|
||||||
<n-icon :component="DownloadOutline" />
|
</template>
|
||||||
</template>
|
刷新数据
|
||||||
导出数据
|
</n-button>
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { NModal, NTabs, NTabPane, NButton, NIcon } from 'naive-ui'
|
import { NModal, NTabs, NTabPane, NButton, NIcon, NSpin } from 'naive-ui'
|
||||||
import { TrendingUpOutline, DownloadOutline } from '@vicons/ionicons5'
|
import { TrendingUpOutline, DownloadOutline, RefreshOutline } from '@vicons/ionicons5'
|
||||||
|
import { httpClient } from '@/utils/http'
|
||||||
|
import type { ApiResponse } from '@/types/http'
|
||||||
|
|
||||||
// 导入子组件
|
// 导入子组件
|
||||||
import SoftwareInfoCard from './SoftwareInfoCard.vue'
|
import SoftwareInfoCard from './SoftwareInfoCard.vue'
|
||||||
@@ -149,14 +163,66 @@ import NetworkRelationChart from './NetworkRelationChart.vue'
|
|||||||
import PortStatsTable from './PortStatsTable.vue'
|
import PortStatsTable from './PortStatsTable.vue'
|
||||||
import ProtocolChart from './ProtocolChart.vue'
|
import ProtocolChart from './ProtocolChart.vue'
|
||||||
|
|
||||||
|
// 数据接口定义
|
||||||
|
interface SoftwareDetail {
|
||||||
|
appId: string
|
||||||
|
processName: string
|
||||||
|
displayName: string
|
||||||
|
processPath: string
|
||||||
|
icon: string
|
||||||
|
version: string
|
||||||
|
company: string
|
||||||
|
totalBytes: number
|
||||||
|
uploadBytes: number
|
||||||
|
downloadBytes: number
|
||||||
|
connectionCount: number
|
||||||
|
percentage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SoftwareInfo {
|
||||||
|
processName: string
|
||||||
|
displayName: string
|
||||||
|
version: string
|
||||||
|
company: string
|
||||||
|
processPath: string
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetworkRelationData {
|
||||||
|
nodes: Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
size: number
|
||||||
|
category: number
|
||||||
|
}>
|
||||||
|
links: Array<{
|
||||||
|
source: string
|
||||||
|
target: string
|
||||||
|
value: number
|
||||||
|
label: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortStats {
|
||||||
|
port: number
|
||||||
|
protocol: string
|
||||||
|
connectionCount: number
|
||||||
|
totalBytes: number
|
||||||
|
remoteHosts: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProtocolData {
|
||||||
|
protocol: string
|
||||||
|
bytes: number
|
||||||
|
percentage: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
// Props定义
|
// Props定义
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
software?: any
|
appId?: string | null
|
||||||
softwareInfo?: any
|
|
||||||
networkRelationData?: any
|
|
||||||
portStatsData?: any[]
|
|
||||||
protocolData?: any[]
|
|
||||||
timeRange?: string
|
timeRange?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -165,27 +231,173 @@ const emit = defineEmits<{
|
|||||||
'update:show': [value: boolean]
|
'update:show': [value: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const softwareDetail = ref<SoftwareDetail | null>(null)
|
||||||
|
const softwareInfo = ref<SoftwareInfo | null>(null)
|
||||||
|
const networkRelationData = ref<NetworkRelationData>({ nodes: [], links: [] })
|
||||||
|
const portStatsData = ref<PortStats[]>([])
|
||||||
|
const protocolData = ref<ProtocolData[]>([])
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.show,
|
get: () => props.show,
|
||||||
set: (value) => emit('update:show', value)
|
set: (value) => emit('update:show', value),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算平均速度
|
// 计算平均速度
|
||||||
const averageSpeed = computed(() => {
|
const averageSpeed = computed(() => {
|
||||||
if (!props.software?.totalBytes) return 0
|
if (!softwareDetail.value?.totalBytes) return 0
|
||||||
// 模拟计算,实际应根据时间范围计算
|
// 模拟计算,实际应根据时间范围计算
|
||||||
return props.software.totalBytes / 3600 // 假设1小时的数据
|
return softwareDetail.value.totalBytes / 3600 // 假设1小时的数据
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// API调用函数
|
||||||
|
const getSoftwareDetail = async () => {
|
||||||
|
if (!props.appId) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
timeRange: props.timeRange || '1hour',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ApiResponse<SoftwareDetail> = await httpClient.get(
|
||||||
|
`/apps/${props.appId}/detail`,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
softwareDetail.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取软件详情失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSoftwareInfo = async () => {
|
||||||
|
if (!props.appId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: ApiResponse<SoftwareInfo> = await httpClient.get(`/apps/${props.appId}/info`)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
softwareInfo.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取软件信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNetworkRelation = async () => {
|
||||||
|
if (!props.appId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
timeRange: props.timeRange || '1hour',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ApiResponse<NetworkRelationData> = await httpClient.get(
|
||||||
|
`/apps/${props.appId}/network-relation`,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
networkRelationData.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取网络关系数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPortStats = async () => {
|
||||||
|
if (!props.appId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
timeRange: props.timeRange || '1hour',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ApiResponse<PortStats[]> = await httpClient.get(
|
||||||
|
`/apps/${props.appId}/port-stats`,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
portStatsData.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取端口统计失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProtocolData = async () => {
|
||||||
|
if (!props.appId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
timeRange: props.timeRange || '1hour',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ApiResponse<ProtocolData[]> = await httpClient.get(
|
||||||
|
`/apps/${props.appId}/protocols`,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
protocolData.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取协议数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有数据
|
||||||
|
const fetchAllData = async () => {
|
||||||
|
if (!props.appId) return
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
getSoftwareDetail(),
|
||||||
|
getSoftwareInfo(),
|
||||||
|
getNetworkRelation(),
|
||||||
|
getPortStats(),
|
||||||
|
getProtocolData(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听AppId和时间范围变化
|
||||||
|
watch(
|
||||||
|
[() => props.appId, () => props.timeRange],
|
||||||
|
() => {
|
||||||
|
if (props.show && props.appId) {
|
||||||
|
fetchAllData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听弹窗显示状态
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && props.appId) {
|
||||||
|
fetchAllData()
|
||||||
|
} else if (!newVal) {
|
||||||
|
// 关闭弹窗时清理数据
|
||||||
|
softwareDetail.value = null
|
||||||
|
softwareInfo.value = null
|
||||||
|
networkRelationData.value = { nodes: [], links: [] }
|
||||||
|
portStatsData.value = []
|
||||||
|
protocolData.value = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 格式化字节数
|
// 格式化字节数
|
||||||
const formatBytes = (bytes: number): string => {
|
const formatBytes = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 B'
|
if (bytes === 0) return '0 B'
|
||||||
|
|
||||||
const k = 1024
|
const k = 1024
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +614,36 @@ const exportData = () => {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 无数据状态 */
|
||||||
|
.no-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 应用图标 */
|
||||||
|
.app-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式 */
|
/* 响应式 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.detail-header {
|
.detail-header {
|
||||||
@@ -409,16 +651,16 @@ const exportData = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-stats {
|
.detail-stats {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-grid,
|
.overview-grid,
|
||||||
.network-grid {
|
.network-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
@@ -434,4 +676,4 @@ const exportData = () => {
|
|||||||
--n-tab-text-color-active: var(--accent-primary);
|
--n-tab-text-color-active: var(--accent-primary);
|
||||||
--n-bar-color: var(--accent-primary);
|
--n-bar-color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,52 +1,166 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="software-ranking-list">
|
<div class="software-ranking-list">
|
||||||
<div class="ranking-header">
|
<div class="ranking-header">
|
||||||
<div class="search-box">
|
<div class="header-top">
|
||||||
<n-input
|
<div class="search-box">
|
||||||
v-model:value="searchQuery"
|
<n-input
|
||||||
placeholder="搜索软件..."
|
v-model:value="searchQuery"
|
||||||
size="small"
|
placeholder="搜索软件..."
|
||||||
clearable
|
size="small"
|
||||||
>
|
clearable
|
||||||
<template #prefix>
|
>
|
||||||
<n-icon :component="SearchOutline" />
|
<template #prefix>
|
||||||
</template>
|
<n-icon :component="SearchOutline" />
|
||||||
</n-input>
|
</template>
|
||||||
|
</n-input>
|
||||||
|
</div>
|
||||||
|
<div class="sort-controls">
|
||||||
|
<n-select
|
||||||
|
v-model:value="sortBy"
|
||||||
|
size="small"
|
||||||
|
:options="sortOptions"
|
||||||
|
placeholder="排序方式"
|
||||||
|
style="width: 120px"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="sortOrder === 'desc' ? 'primary' : 'default'"
|
||||||
|
@click="toggleSortOrder"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="sortOrder === 'desc' ? ArrowDownOutline : ArrowUpOutline" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表头部标题 -->
|
||||||
|
<div class="list-headers">
|
||||||
|
<!-- 左侧排名和图标区域 -->
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="rank-header-cell">#</div>
|
||||||
|
<div class="icon-header-cell"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间软件信息区域 -->
|
||||||
|
<div class="header-middle">
|
||||||
|
<div class="app-header-cell">软件信息</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧统计数据区域 -->
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="stats-headers">
|
||||||
|
<div class="stat-header-cell">总流量</div>
|
||||||
|
<div class="stat-header-cell">上传</div>
|
||||||
|
<div class="stat-header-cell">连接数</div>
|
||||||
|
<div class="stat-header-cell">占比</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container" ref="listContainer">
|
<div class="list-container" ref="listContainer" :style="{ height: containerHeight }">
|
||||||
<n-data-table
|
<div class="software-list">
|
||||||
:columns="columns"
|
<div
|
||||||
:data="filteredData"
|
v-for="item in filteredAndSortedData"
|
||||||
:row-key="(row: SoftwareRankingItem) => `${row.processName}_${row.rank}_${row.totalBytes}`"
|
:key="item.appId"
|
||||||
:row-class-name="(row: SoftwareRankingItem) => selectedSoftware?.processName === row.processName ? 'selected-row' : ''"
|
class="software-item"
|
||||||
:on-update:checked-row-keys="() => {}"
|
@click="selectSoftware(item)"
|
||||||
@row-click="selectSoftware"
|
>
|
||||||
striped
|
<!-- 左侧排名和图标 -->
|
||||||
size="small"
|
<div class="item-left">
|
||||||
:scroll-x="800"
|
<div class="rank-badge" :class="getRankClass(item.rank)">
|
||||||
:max-height="containerHeight"
|
{{ item.rank }}
|
||||||
/>
|
</div>
|
||||||
|
<div class="app-icon-container">
|
||||||
|
<img
|
||||||
|
v-if="item.icon"
|
||||||
|
:src="`data:image/png;base64,${item.icon}`"
|
||||||
|
class="app-icon"
|
||||||
|
:alt="item.displayName"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="app-icon-placeholder"
|
||||||
|
:style="getGradientColor(item.displayName.charAt(0).toUpperCase())"
|
||||||
|
>
|
||||||
|
{{ item.displayName.charAt(0).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间软件信息 -->
|
||||||
|
<div class="item-middle">
|
||||||
|
<div class="app-main-info">
|
||||||
|
<div class="app-name-line">
|
||||||
|
<span class="app-name">{{ item.displayName }}</span>
|
||||||
|
<span v-if="item.version" class="version">v{{ item.version }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-meta-line">
|
||||||
|
<span class="process-name">{{ item.processName }}</span>
|
||||||
|
<span v-if="item.company" class="company">{{ item.company }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-path-line" v-if="item.processPath">
|
||||||
|
<span class="path-label">路径:</span>
|
||||||
|
<span class="path-value">{{ item.processPath }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧统计数据 -->
|
||||||
|
<div class="item-right">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value primary">{{ formatBytes(item.totalBytes) }}</div>
|
||||||
|
<div class="stat-label">总流量</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">{{ formatBytes(item.uploadBytes || 0) }}</div>
|
||||||
|
<div class="stat-label">上传</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">{{ item.connectionCount }}</div>
|
||||||
|
<div class="stat-label">连接数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value percent">{{ item.percentage.toFixed(1) }}%</div>
|
||||||
|
<div class="stat-label">占比</div>
|
||||||
|
<div class="percentage-bar">
|
||||||
|
<div
|
||||||
|
class="percentage-fill"
|
||||||
|
:style="{ width: `${Math.min(item.percentage, 100)}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="filteredAndSortedData.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">📊</div>
|
||||||
|
<div class="empty-text">{{ searchQuery ? '未找到匹配的软件' : '暂无数据' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数据统计信息 -->
|
<!-- 数据统计信息 -->
|
||||||
<div class="data-info">
|
<div class="data-info">
|
||||||
<span>共 {{ filteredData.length }} 条数据</span>
|
<span>共 {{ filteredAndSortedData.length }} 条数据</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick, h } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import { NInput, NIcon, NButton, NDataTable, NAvatar, NTag, NTooltip } from 'naive-ui'
|
import { NInput, NIcon, NButton, NSelect } from 'naive-ui'
|
||||||
import { SearchOutline } from '@vicons/ionicons5'
|
import { SearchOutline, ArrowUpOutline, ArrowDownOutline } from '@vicons/ionicons5'
|
||||||
import type { DataTableColumns } from 'naive-ui'
|
|
||||||
import { getGradientColor } from '@/utils/colorUtils'
|
import { getGradientColor } from '@/utils/colorUtils'
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
interface SoftwareRankingItem {
|
interface SoftwareRankingItem {
|
||||||
rank: number
|
rank: number
|
||||||
|
appId: string
|
||||||
processName: string
|
processName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
processPath?: string
|
processPath?: string
|
||||||
@@ -64,7 +178,6 @@ interface SoftwareRankingItem {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: SoftwareRankingItem[]
|
data: SoftwareRankingItem[]
|
||||||
timeRange: string
|
timeRange: string
|
||||||
selectedSoftware?: SoftwareRankingItem | null
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
@@ -75,6 +188,17 @@ const emit = defineEmits<{
|
|||||||
// 响应式数据
|
// 响应式数据
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const listContainer = ref<HTMLElement>()
|
const listContainer = ref<HTMLElement>()
|
||||||
|
const sortBy = ref('totalBytes')
|
||||||
|
const sortOrder = ref<'asc' | 'desc'>('desc')
|
||||||
|
|
||||||
|
// 排序选项
|
||||||
|
const sortOptions = [
|
||||||
|
{ label: '总流量', value: 'totalBytes' },
|
||||||
|
{ label: '上传流量', value: 'uploadBytes' },
|
||||||
|
{ label: '连接数', value: 'connectionCount' },
|
||||||
|
{ label: '占比', value: 'percentage' },
|
||||||
|
{ label: '排名', value: 'rank' }
|
||||||
|
]
|
||||||
|
|
||||||
// 计算表格容器高度
|
// 计算表格容器高度
|
||||||
const containerHeight = computed(() => {
|
const containerHeight = computed(() => {
|
||||||
@@ -87,134 +211,44 @@ const containerHeight = computed(() => {
|
|||||||
return '400px'
|
return '400px'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表格列定义
|
// 切换排序顺序
|
||||||
const columns: DataTableColumns<SoftwareRankingItem> = [
|
const toggleSortOrder = () => {
|
||||||
{
|
sortOrder.value = sortOrder.value === 'desc' ? 'asc' : 'desc'
|
||||||
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 filteredAndSortedData = computed(() => {
|
||||||
let filtered = props.data
|
let filtered = props.data
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
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))
|
(item.company && item.company.toLowerCase().includes(query))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不再限制显示数量,显示所有数据
|
// 排序
|
||||||
return filtered
|
const sorted = [...filtered].sort((a, b) => {
|
||||||
|
let aValue = a[sortBy.value as keyof SoftwareRankingItem] as number
|
||||||
|
let bValue = b[sortBy.value as keyof SoftwareRankingItem] as number
|
||||||
|
|
||||||
|
// 处理可能为 undefined 的值
|
||||||
|
if (sortBy.value === 'uploadBytes') {
|
||||||
|
aValue = a.uploadBytes || 0
|
||||||
|
bValue = b.uploadBytes || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder.value === 'desc') {
|
||||||
|
return bValue - aValue
|
||||||
|
} else {
|
||||||
|
return aValue - bValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return sorted
|
||||||
})
|
})
|
||||||
|
|
||||||
// 选择软件
|
// 选择软件
|
||||||
@@ -232,11 +266,11 @@ const getRankClass = (rank: number): string => {
|
|||||||
// 格式化字节数
|
// 格式化字节数
|
||||||
const formatBytes = (bytes: number): string => {
|
const formatBytes = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 B'
|
if (bytes === 0) return '0 B'
|
||||||
|
|
||||||
const k = 1024
|
const k = 1024
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,50 +289,146 @@ watch(() => props.timeRange, () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: var(--bg-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-header {
|
.ranking-header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid var(--border-secondary);
|
border-bottom: 1px solid var(--border-secondary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 8px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-info {
|
.sort-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
padding: 8px;
|
gap: 8px;
|
||||||
border-top: 1px solid var(--border-secondary);
|
flex-shrink: 0;
|
||||||
margin-top: 8px;
|
}
|
||||||
font-size: 12px;
|
|
||||||
|
.list-headers {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格内容样式 */
|
.header-left {
|
||||||
:deep(.n-data-table-td) {
|
display: flex;
|
||||||
padding: 8px 12px !important;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.selected-row) {
|
.rank-header-cell {
|
||||||
background-color: var(--bg-selected) !important;
|
width: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-header-cell {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-middle {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header-cell {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-headers {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-header-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 55px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge {
|
.rank-badge {
|
||||||
width: 24px;
|
width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -306,107 +436,312 @@ watch(() => props.timeRange, () => {
|
|||||||
.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 8px rgba(255, 215, 0, 0.3);
|
box-shadow: 0 2px 12px rgba(255, 215, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge.top-ten {
|
.rank-badge.top-ten {
|
||||||
background: linear-gradient(135deg, #c0c0c0, #e5e7eb);
|
background: linear-gradient(135deg, #c0c0c0, #e5e7eb);
|
||||||
color: #374151;
|
color: #374151;
|
||||||
|
box-shadow: 0 2px 8px rgba(192, 192, 192, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge.normal {
|
.rank-badge.normal {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--border-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-column {
|
.app-icon-container {
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-icon {
|
.app-icon {
|
||||||
width: 28px;
|
width: 32px;
|
||||||
height: 28px;
|
height: 32px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-icon-span {
|
.app-icon-placeholder {
|
||||||
width: 28px;
|
width: 32px;
|
||||||
height: 28px;
|
height: 32px;
|
||||||
background: linear-gradient(135deg, var(--accent-primary) 0%, #1d4ed8 100%);
|
|
||||||
border-radius: 6px;
|
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);
|
font-size: 14px;
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon-span span {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: white;
|
color: white;
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.software-name-cell {
|
.item-middle {
|
||||||
font-size: 13px;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.percentage-cell {
|
.version {
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-quaternary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-meta-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-name {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-path-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-value {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-right {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
min-width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.percentage-text {
|
.stat-value {
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.primary {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.percent {
|
||||||
|
color: var(--accent-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.percentage-bar {
|
.percentage-bar {
|
||||||
width: 40px;
|
width: 36px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 auto;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.percentage-fill {
|
.percentage-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
background: linear-gradient(90deg, var(--accent-success), #10b981);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格样式覆盖 */
|
.empty-state {
|
||||||
:deep(.n-data-table) {
|
display: flex;
|
||||||
--n-border-color: var(--border-secondary);
|
flex-direction: column;
|
||||||
--n-th-color: var(--bg-tertiary);
|
align-items: center;
|
||||||
--n-td-color: var(--bg-card);
|
justify-content: center;
|
||||||
--n-td-color-hover: var(--bg-hover);
|
padding: 60px 20px;
|
||||||
--n-text-color: var(--text-primary);
|
color: var(--text-muted);
|
||||||
--n-th-text-color: var(--text-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-data-table-thead) {
|
.empty-icon {
|
||||||
background: var(--bg-secondary);
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-data-table-tr:hover) {
|
.empty-text {
|
||||||
cursor: pointer;
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-data-table-td) {
|
.data-info {
|
||||||
border-bottom: 1px solid var(--border-tertiary) !important;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--border-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-headers {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-middle {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-middle {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
min-width: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-header-cell {
|
||||||
|
min-width: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-top {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-headers {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-left {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-middle {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-right {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-path-line {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user