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']
|
||||
IconSupport: typeof import('./src/components/icons/IconSupport.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']
|
||||
NHighlight: typeof import('naive-ui')['NHighlight']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
v-model:value="selectedInterface"
|
||||
:options="interfaceOptions"
|
||||
size="small"
|
||||
style="min-width: 120px;"
|
||||
:style="{ minWidth: interfaceSelectWidth }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,26 +121,14 @@
|
||||
|
||||
<!-- 软件流量分析区域 -->
|
||||
<div class="software-analysis-area">
|
||||
<div class="software-ranking-panel">
|
||||
<div class="software-ranking-panel" :class="{ 'large-screen': isLargeScreen }">
|
||||
<div class="panel-header">
|
||||
<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 class="ranking-content">
|
||||
<SoftwareRankingList
|
||||
:data="softwareRankingData"
|
||||
:time-range="selectedRankingRange"
|
||||
:time-range="selectedTimeRange"
|
||||
@select-software="showSoftwareDetail"
|
||||
/>
|
||||
</div>
|
||||
@@ -155,7 +143,7 @@
|
||||
:network-relation-data="networkRelationData"
|
||||
:port-stats-data="portStatsData"
|
||||
:protocol-data="protocolData"
|
||||
:time-range="selectedRankingRange"
|
||||
:time-range="selectedTimeRange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,6 +160,8 @@ import {
|
||||
AlertCircleOutline,
|
||||
ServerOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import { httpClient } from '@/utils/http'
|
||||
import type { ApiResponse } from '@/types/http'
|
||||
|
||||
// 导入组件
|
||||
import TrafficTrendChart from './components/TrafficTrendChart.vue'
|
||||
@@ -179,68 +169,255 @@ import TopAppsChart from './components/TopAppsChart.vue'
|
||||
import SoftwareRankingList from './components/SoftwareRankingList.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: '1day', name: '24小时', available: true },
|
||||
{ type: '7days', name: '7天', available: true },
|
||||
{ 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([
|
||||
{ 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 rankingTimeRanges = ref(['1小时', '1天', '7天', '30天'])
|
||||
const selectedRankingRange = ref('1天')
|
||||
// 大屏幕检测
|
||||
const isLargeScreen = computed(() => {
|
||||
if (typeof window === 'undefined') return false
|
||||
return window.innerHeight >= 900 // 高度大于900px认为是大屏幕
|
||||
})
|
||||
|
||||
// 移除单独的软件排行时间范围,使用顶部统一的时间选择
|
||||
|
||||
// 选中的软件和弹窗状态
|
||||
const selectedSoftware = ref<any>(null)
|
||||
const showDetailModal = ref(false)
|
||||
|
||||
// Mock数据
|
||||
const trafficTrendData = ref([
|
||||
{ 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 trafficTrendData = ref<any[]>([])
|
||||
|
||||
const topAppsData = ref([
|
||||
{ processName: 'chrome.exe', displayName: 'Google Chrome', totalBytes: 1073741824, percentage: 45.2 },
|
||||
{ processName: 'teams.exe', displayName: 'Microsoft Teams', totalBytes: 536870912, percentage: 22.5 },
|
||||
{ processName: 'spotify.exe', displayName: 'Spotify', totalBytes: 268435456, percentage: 11.3 },
|
||||
{ processName: 'code.exe', displayName: 'VS Code', totalBytes: 134217728, percentage: 5.6 },
|
||||
{ processName: 'slack.exe', displayName: 'Slack', totalBytes: 67108864, percentage: 2.8 },
|
||||
])
|
||||
const getTrafficTrends = async () => {
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
timeRange: selectedTimeRange.value,
|
||||
interfaceId: selectedInterface.value
|
||||
}
|
||||
|
||||
const softwareRankingData = ref([
|
||||
{
|
||||
rank: 1,
|
||||
processName: 'chrome.exe',
|
||||
displayName: 'Google Chrome',
|
||||
totalBytes: 1073741824,
|
||||
percentage: 45.2,
|
||||
connectionCount: 23
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
processName: 'teams.exe',
|
||||
displayName: 'Microsoft Teams',
|
||||
totalBytes: 536870912,
|
||||
percentage: 22.5,
|
||||
connectionCount: 12
|
||||
},
|
||||
// ... 更多数据
|
||||
])
|
||||
const res: ApiResponse<TrafficTrend> = await httpClient.get('/traffic/trends', params)
|
||||
if (res.success && res.data) {
|
||||
// 转换 API 数据为图表需要的格式并按时间排序(最新时间在右侧)
|
||||
let processedData = res.data.points
|
||||
.map(point => ({
|
||||
timestamp: parseInt(point.timestamp) * 1000, // 转换为毫秒时间戳
|
||||
uploadSpeed: point.uploadSpeed,
|
||||
downloadSpeed: point.downloadSpeed
|
||||
}))
|
||||
.sort((a, b) => a.timestamp - b.timestamp) // 按时间升序排列,最新时间在右侧
|
||||
|
||||
// 限制显示的数据点数量,避免图表过于密集
|
||||
const maxPoints = 250 // 最多显示250个点
|
||||
if (processedData.length > maxPoints) {
|
||||
// 等间隔采样数据点
|
||||
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(() => {
|
||||
@@ -295,22 +472,35 @@ const showSoftwareDetail = (software: any) => {
|
||||
showDetailModal.value = true
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
// 刷新数据的逻辑
|
||||
const refreshData = async () => {
|
||||
// 刷新所有数据
|
||||
console.log('Refreshing data...')
|
||||
await Promise.all([
|
||||
getAvailableRanges(),
|
||||
getNetworkInterfaces(),
|
||||
getTrafficTrends(),
|
||||
getTopApps(),
|
||||
getSoftwareRanking()
|
||||
])
|
||||
}
|
||||
|
||||
// 监听时间范围变化
|
||||
watch(selectedTimeRange, () => {
|
||||
// 重新加载数据
|
||||
// 重新加载相关数据
|
||||
getTrafficTrends()
|
||||
getTopApps()
|
||||
getSoftwareRanking()
|
||||
})
|
||||
|
||||
watch(selectedInterface, () => {
|
||||
// 重新加载趋势数据
|
||||
getTrafficTrends()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 页面初始化,不再默认选择软件
|
||||
|
||||
onMounted(async () => {
|
||||
// 页面初始化,加载所有数据
|
||||
await refreshData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -632,6 +822,11 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.software-ranking-panel.large-screen {
|
||||
height: calc(100vh - 400px); /* 大屏幕下使用更多高度 */
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
|
||||
@@ -15,57 +15,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ranking-list" ref="listContainer">
|
||||
<div
|
||||
v-for="(item, index) in filteredData"
|
||||
:key="item.processName"
|
||||
:class="['ranking-item', { 'selected': selectedSoftware?.processName === item.processName }]"
|
||||
@click="selectSoftware(item)"
|
||||
>
|
||||
<div class="rank-badge" :class="getRankClass(item.rank)">
|
||||
{{ item.rank }}
|
||||
</div>
|
||||
|
||||
<div class="software-icon">
|
||||
<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 class="table-container" ref="listContainer">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="filteredData"
|
||||
:row-key="(row: SoftwareRankingItem) => `${row.processName}_${row.rank}_${row.totalBytes}`"
|
||||
:row-class-name="(row: SoftwareRankingItem) => selectedSoftware?.processName === row.processName ? 'selected-row' : ''"
|
||||
:on-update:checked-row-keys="() => {}"
|
||||
@row-click="selectSoftware"
|
||||
striped
|
||||
size="small"
|
||||
:scroll-x="800"
|
||||
:max-height="containerHeight"
|
||||
/>
|
||||
|
||||
<!-- 加载更多指示器 -->
|
||||
<div v-if="hasMore" class="load-more" @click="loadMore">
|
||||
<n-button text size="small">
|
||||
加载更多...
|
||||
</n-button>
|
||||
<!-- 数据统计信息 -->
|
||||
<div class="data-info">
|
||||
<span>共 {{ filteredData.length }} 条数据</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { NInput, NIcon, NButton } from 'naive-ui'
|
||||
import { ref, computed, watch, nextTick, h } from 'vue'
|
||||
import { NInput, NIcon, NButton, NDataTable, NAvatar, NTag, NTooltip } from 'naive-ui'
|
||||
import { SearchOutline } from '@vicons/ionicons5'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { getGradientColor } from '@/utils/colorUtils'
|
||||
|
||||
// 接口定义
|
||||
interface SoftwareRankingItem {
|
||||
@@ -73,6 +50,9 @@ interface SoftwareRankingItem {
|
||||
processName: string
|
||||
displayName: string
|
||||
processPath?: string
|
||||
icon?: string
|
||||
version?: string
|
||||
company?: string
|
||||
totalBytes: number
|
||||
uploadBytes?: number
|
||||
downloadBytes?: number
|
||||
@@ -95,8 +75,130 @@ const emit = defineEmits<{
|
||||
// 响应式数据
|
||||
const searchQuery = ref('')
|
||||
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(() => {
|
||||
@@ -106,11 +208,13 @@ const filteredData = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(item =>
|
||||
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)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
displayCount.value += 20
|
||||
}
|
||||
|
||||
// 获取排名样式类
|
||||
const getRankClass = (rank: number): string => {
|
||||
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]}`
|
||||
}
|
||||
|
||||
// 重置显示数量当时间范围改变时
|
||||
// 重置滚动位置当时间范围改变时
|
||||
watch(() => props.timeRange, () => {
|
||||
displayCount.value = 20
|
||||
nextTick(() => {
|
||||
if (listContainer.value) {
|
||||
listContainer.value.scrollTop = 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 重置显示数量当搜索查询改变时
|
||||
watch(searchQuery, () => {
|
||||
displayCount.value = 20
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -174,42 +267,38 @@ watch(searchQuery, () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
.data-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ranking-item:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-hover);
|
||||
/* 表格内容样式 */
|
||||
:deep(.n-data-table-td) {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.ranking-item.selected {
|
||||
background: var(--bg-selected);
|
||||
border-color: var(--accent-primary);
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--bg-selected) !important;
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -217,7 +306,7 @@ watch(searchQuery, () => {
|
||||
.rank-badge.top-three {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
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 {
|
||||
@@ -230,109 +319,94 @@ watch(searchQuery, () => {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.software-icon {
|
||||
flex-shrink: 0;
|
||||
.icon-column {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
.app-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
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;
|
||||
align-items: 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;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.software-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.software-name {
|
||||
font-size: 14px;
|
||||
.software-name-cell {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.software-details {
|
||||
.percentage-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.percentage-text {
|
||||
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;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.traffic-percentage {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.traffic-bar {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
.percentage-bar {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.traffic-fill {
|
||||
.percentage-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
margin-top: 8px;
|
||||
/* 表格样式覆盖 */
|
||||
:deep(.n-data-table) {
|
||||
--n-border-color: var(--border-secondary);
|
||||
--n-th-color: var(--bg-tertiary);
|
||||
--n-td-color: var(--bg-card);
|
||||
--n-td-color-hover: var(--bg-hover);
|
||||
--n-text-color: var(--text-primary);
|
||||
--n-th-text-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.ranking-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
:deep(.n-data-table-thead) {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.ranking-list::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
:deep(.n-data-table-tr:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ranking-list::-webkit-scrollbar-thumb {
|
||||
background: var(--border-secondary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ranking-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
:deep(.n-data-table-td) {
|
||||
border-bottom: 1px solid var(--border-tertiary) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user