U:调用真实UI
This commit is contained in:
1 parent
3e7ca51ab0
commit
1732d279d9
3 files changed
+475
-205
No files matched your search
+1
@@ -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>
|
||||||
Reference in new issue
Block a user