优化调整、BT下载(有bug)
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
import {
|
||||
Globe, Play, Square, RotateCw, Power, Zap, Plus, RefreshCw, Trash2,
|
||||
Check, AlertCircle, Server, Settings as SettingsIcon, ListChecks,
|
||||
Upload, Link2, Loader2, Download, Timer, Target, FolderOpen, Copy, DownloadCloud
|
||||
Upload, Link2, Loader2, Download, Timer, Target, FolderOpen, Copy, DownloadCloud,
|
||||
Waypoints, ArrowDown, ArrowUp, Activity, X
|
||||
} from '@lucide/vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
@@ -10,7 +11,7 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { appDataDir } from '@tauri-apps/api/path'
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { useProxyStore, type ProxyNode } from '@/stores/proxyStore'
|
||||
import { useProxyStore, type ProxyNode, type ProxyConnection } from '@/stores/proxyStore'
|
||||
import { useModuleTabs } from '@/lib/use-module-tabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
@@ -84,6 +85,7 @@ const activeTab = ref('overview')
|
||||
const tabsStore = useModuleTabsStore()
|
||||
const tabsListRef = useModuleTabs('proxy', activeTab, [
|
||||
{ value: 'overview', label: '概览' },
|
||||
{ value: 'connections', label: '连接' },
|
||||
{ value: 'proxies', label: '节点' },
|
||||
{ value: 'profiles', label: '订阅' },
|
||||
{ value: 'settings', label: '设置' }
|
||||
@@ -141,9 +143,179 @@ const accordionValue = ref<string>('')
|
||||
|
||||
// 进程状态轮询
|
||||
let statusTimer: ReturnType<typeof setInterval> | null = null
|
||||
let trafficTimer: ReturnType<typeof setInterval> | null = null
|
||||
let connTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 字节 → 人类可读大小(B / KB / MB / GB / TB) */
|
||||
function fmtBytes(v: number): string {
|
||||
if (!v && v !== 0) return '--'
|
||||
if (v < 1024) return v + ' B'
|
||||
const units = ['KB', 'MB', 'GB', 'TB']
|
||||
let n = v / 1024
|
||||
let u = 0
|
||||
while (n >= 1024 && u < units.length - 1) {
|
||||
n /= 1024
|
||||
u++
|
||||
}
|
||||
return (n >= 100 ? n.toFixed(0) : n >= 10 ? n.toFixed(1) : n.toFixed(2)) + ' ' + units[u]
|
||||
}
|
||||
|
||||
/** 速率显示(字节/秒 → /s) */
|
||||
function fmtSpeed(v: number): string {
|
||||
return fmtBytes(v) + '/s'
|
||||
}
|
||||
|
||||
const running = computed(() => store.status.running)
|
||||
|
||||
// ===== 连接页签 =====
|
||||
/** 当前连接列表(store.connections 可能为 null → 视为空) */
|
||||
const connList = computed(() => store.connections ?? [])
|
||||
const connFilter = ref('')
|
||||
/** 内网/国内/国外 一键过滤('all' = 全部) */
|
||||
const connScopeFilter = ref<'all' | ConnScope>('all')
|
||||
/** 一键过滤选项 */
|
||||
const scopeFilterOptions: { value: 'all' | ConnScope; label: string }[] = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'direct', label: '国内' },
|
||||
{ value: 'proxy', label: '国外' }
|
||||
]
|
||||
/** 顶部下载/上传速率(复用实时流量快照的整体速率) */
|
||||
const connTotalDownloadSec = computed(() => store.traffic?.downloadSpeed ?? 0)
|
||||
const connTotalUploadSec = computed(() => store.traffic?.uploadSpeed ?? 0)
|
||||
/** 命中规则总数 = 活跃连接数(每条连接命中一条规则) */
|
||||
const ruleHitCount = computed(() => connList.value.length)
|
||||
/** 按规则聚合当前连接,便于观察哪些规则被频繁命中 */
|
||||
const ruleHits = computed(() => {
|
||||
const map = new Map<string, number>()
|
||||
for (const c of connList.value) {
|
||||
const r = c.rule || 'DIRECT'
|
||||
map.set(r, (map.get(r) ?? 0) + 1)
|
||||
}
|
||||
return [...map.entries()]
|
||||
.map(([rule, count]) => ({ rule, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
})
|
||||
/** 按内网/国内/国外、通用关键词过滤后的连接 */
|
||||
const filteredConnections = computed(() => {
|
||||
const q = connFilter.value.trim().toLowerCase()
|
||||
return connList.value.filter((c) => {
|
||||
if (connScopeFilter.value !== 'all' && connScopeOf(c) !== connScopeFilter.value) return false
|
||||
if (!q) return true
|
||||
const process = (c.metadata?.process ?? '').toLowerCase()
|
||||
const host = (c.metadata?.host ?? '').toLowerCase()
|
||||
const rule = (c.rule ?? '').toLowerCase()
|
||||
return process.includes(q) || host.includes(q) || rule.includes(q)
|
||||
})
|
||||
})
|
||||
/** 连接进程显示名 */
|
||||
const connProcess = (c: ProxyConnection) => c.metadata?.process || '未知'
|
||||
/** 连接源地址显示(IP:端口) */
|
||||
const connSource = (c: ProxyConnection) => {
|
||||
const ip = c.metadata?.sourceIP
|
||||
const port = c.metadata?.sourcePort
|
||||
return ip ? `${ip}${port ? ':' + port : ''}` : '--'
|
||||
}
|
||||
/** 连接目标显示:优先 host,否则用 IP:端口 */
|
||||
const connHost = (c: ProxyConnection) => {
|
||||
const h = c.metadata?.host
|
||||
if (h) return h
|
||||
const ip = c.metadata?.destinationIP
|
||||
const port = c.metadata?.destinationPort
|
||||
return ip ? `${ip}${port ? ':' + port : ''}` : '--'
|
||||
}
|
||||
|
||||
// ===== 规则中文名 / 内外网判断 =====
|
||||
/** 规则类型归一化:忽略大小写与 "-" "_" 空格(订阅里可能写成 DomainSuffix / DOMAIN-SUFFIX) */
|
||||
const normRuleType = (s: string) => s.trim().replace(/[-_\s]/g, '').toLowerCase()
|
||||
const RULE_CN: Record<string, string> = {
|
||||
// 匹配/动作
|
||||
match: '兜底', final: '兜底', ruleset: '规则集', direct: '直连', reject: '拒绝',
|
||||
// 域名
|
||||
domain: '域名', domainsuffix: '域名后缀', domainkeyword: '域名关键字', domainregex: '域名正则',
|
||||
// 地理 / 站点
|
||||
geoip: '地区', geosite: '域名组', ipasn: 'ASN',
|
||||
// 地址网段
|
||||
ipcidr: 'IP段', ipcidr6: 'IP段(v6)', srcipcidr: '源IP段', srcipcidr6: '源IP段(v6)',
|
||||
dstnet: '目标地址', srcnet: '源地址', network: '网络类型',
|
||||
// 端口
|
||||
srcport: '源端口', dstport: '目标端口', srcportrange: '源端口范围', dstportrange: '目标端口范围',
|
||||
// 进程 / 用户
|
||||
process: '进程', processname: '进程名', processpath: '进程路径', processpathregex: '进程路径正则', uid: '用户ID',
|
||||
// 入站
|
||||
intype: '入站类型', inuser: '入站用户', inname: '入站名称', inport: '入站端口',
|
||||
// 规则集衍生
|
||||
rulesetipcidr: '规则集IP', rulesetipcidr6: '规则集IP(v6)', rulesetdomainsuffix: '规则集域名后缀',
|
||||
rulesetdomainkeyword: '规则集域名关键字', rulesetdomainregex: '规则集域名正则', rulesetgeoip: '规则集地区',
|
||||
// 逻辑
|
||||
and: '与', not: '非', or: '或', subrule: '子规则'
|
||||
}
|
||||
/** 将 mihomo 规则翻译为中文类型名(仅替换类型关键字,保留匹配内容) */
|
||||
const translateRule = (rule: string): string => {
|
||||
const parts = rule.split(',')
|
||||
const mapped = RULE_CN[normRuleType(parts[0])]
|
||||
if (!mapped) return rule
|
||||
return [mapped, ...parts.slice(1)].join(',')
|
||||
}
|
||||
/**
|
||||
* 连接走向分类:只区分国内/国外。
|
||||
* - direct 国内(未走代理;内网/局域网因代理过滤也已直连,归入国内)
|
||||
* - proxy 国外(已走代理节点;代理多用于访问境外,故视为国外)
|
||||
* 判定依据:链路最后一跳是否为 DIRECT。
|
||||
*/
|
||||
type ConnScope = 'direct' | 'proxy'
|
||||
const connScopeOf = (c: ProxyConnection): ConnScope => {
|
||||
const chain = c.chains
|
||||
if (chain && chain.length) {
|
||||
return chain[chain.length - 1] === 'DIRECT' ? 'direct' : 'proxy'
|
||||
}
|
||||
// 退化:无链路信息时按国内直连兜底
|
||||
return 'direct'
|
||||
}
|
||||
|
||||
/** 各规则的简短释义(供「规则命中」展示),仅为便于理解,非精确语义 */
|
||||
const RULE_DESC: Record<string, string> = {
|
||||
// 匹配/动作
|
||||
match: '未匹配任何规则时的兜底', final: '未匹配任何规则时的兜底', direct: '直连', reject: '拒绝访问',
|
||||
// 域名
|
||||
domain: '完全匹配该域名', domainsuffix: '匹配该域名及其子域名', domainkeyword: '域名包含该关键词', domainregex: '域名按正则匹配',
|
||||
// 地理 / 站点
|
||||
geoip: '按 IP 所属国家/地区', geosite: '按域名所属站点类别', ipasn: '按 IP 所属 ASN 自治域',
|
||||
// 地址网段
|
||||
ipcidr: '匹配该 IP 网段', ipcidr6: '匹配该 IPv6 网段', srcipcidr: '按源 IP 网段', srcipcidr6: '按源 IPv6 网段',
|
||||
dstnet: '按目标 IP/域名', srcnet: '按源 IP/域名', network: '按网络类型(TCP/UDP)',
|
||||
// 端口
|
||||
srcport: '按源端口', dstport: '按目标端口', srcportrange: '按源端口范围', dstportrange: '按目标端口范围',
|
||||
// 进程 / 用户
|
||||
process: '按进程', processname: '按进程名', processpath: '按进程可执行路径', processpathregex: '按进程路径正则',
|
||||
uid: '按 Linux 用户 ID',
|
||||
// 入站
|
||||
intype: '按入站类型', inuser: '按入站用户', inname: '按入站名称', inport: '按入站端口',
|
||||
// 规则集衍生
|
||||
ruleset: '按规则集内容匹配',
|
||||
rulesetipcidr: '匹配规则集中任一 IP 网段', rulesetipcidr6: '匹配规则集中任一 IPv6 网段',
|
||||
rulesetdomainsuffix: '匹配规则集中任一域名后缀', rulesetdomainkeyword: '匹配规则集中任一域名关键字',
|
||||
rulesetdomainregex: '匹配规则集正则', rulesetgeoip: '匹配规则集中任一地区',
|
||||
// 逻辑
|
||||
and: '多个条件同时满足(与)', or: '任一条件满足(或)', not: '取反(非)', subrule: '子规则分发'
|
||||
}
|
||||
/** 取了某条规则的类型释义;未知类型返回空串 */
|
||||
const ruleDesc = (rule: string): string => {
|
||||
return RULE_DESC[normRuleType(rule.split(',')[0])] ?? ''
|
||||
}
|
||||
/** 断开全部连接 */
|
||||
const closeAllConnections = async () => {
|
||||
const ok = await showConfirm({
|
||||
title: '断开全部连接',
|
||||
description: `确定断开当前 ${connList.value.length} 条活跃连接吗?`,
|
||||
confirmText: '断开',
|
||||
destructive: true
|
||||
})
|
||||
if (!ok) return
|
||||
for (const c of [...connList.value]) {
|
||||
await store.closeConnection(c.id).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
// 伪节点关键词:DIRECT/REJECT/流量/套餐等非具体代理节点
|
||||
const PSEUDO_NODE_KEYWORDS = [
|
||||
'DIRECT', 'REJECT', 'PASS', 'COMPATIBLE',
|
||||
@@ -353,6 +525,16 @@ onMounted(() => {
|
||||
// 同步系统代理真实状态(注册表可能被外部改动,3s 周期足够感知)
|
||||
await store.refreshSystemProxy()
|
||||
}, 3000)
|
||||
// 流量采样:运行中每秒拉取一次实时速率/累计流量
|
||||
trafficTimer = setInterval(async () => {
|
||||
if (document.hidden) return
|
||||
if (running.value) await store.refreshTraffic()
|
||||
}, 1000)
|
||||
// 连接列表:仅「连接」页签激活且运行时低频拉取
|
||||
connTimer = setInterval(async () => {
|
||||
if (document.hidden) return
|
||||
if (activeTab.value === 'connections' && running.value) await store.refreshConnections()
|
||||
}, 3000)
|
||||
// 页面重新可见时立即刷新一次系统代理状态(切回标签页/从托盘返回主窗口)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
// 监听后端自动切换节点完成事件(后台执行,不依赖模块激活)
|
||||
@@ -363,6 +545,8 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusTimer) clearInterval(statusTimer)
|
||||
if (trafficTimer) clearInterval(trafficTimer)
|
||||
if (connTimer) clearInterval(connTimer)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
autoSwitchUnlisten.forEach(fn => fn())
|
||||
autoSwitchUnlisten = []
|
||||
@@ -960,8 +1144,9 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
<div class="h-full p-6">
|
||||
<Tabs v-model="activeTab" class="h-full flex flex-col">
|
||||
<div ref="tabsListRef">
|
||||
<TabsList class="grid w-full grid-cols-4 max-w-md !bg-transparent !p-0 !shadow-none">
|
||||
<TabsList class="grid w-full grid-cols-5 max-w-md !bg-transparent !p-0 !shadow-none">
|
||||
<TabsTrigger value="overview" class="gap-1.5"><Globe class="size-3.5" />概览</TabsTrigger>
|
||||
<TabsTrigger value="connections" class="gap-1.5"><Waypoints class="size-3.5" />连接</TabsTrigger>
|
||||
<TabsTrigger value="proxies" class="gap-1.5"><Server class="size-3.5" />节点</TabsTrigger>
|
||||
<TabsTrigger value="profiles" class="gap-1.5"><ListChecks class="size-3.5" />订阅</TabsTrigger>
|
||||
<TabsTrigger value="settings" class="gap-1.5"><SettingsIcon class="size-3.5" />设置</TabsTrigger>
|
||||
@@ -972,6 +1157,42 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
<TabsContent value="overview" class="flex-1 mt-4 tab-animate">
|
||||
<ScrollArea class="h-full pr-3">
|
||||
<div class="columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid">
|
||||
<!-- 实时流量 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center justify-between text-base">
|
||||
<span class="flex items-center gap-2"><Activity class="size-4 text-primary" />实时流量</span>
|
||||
<Badge v-if="running" variant="outline" class="gap-1 text-xs">
|
||||
<span class="size-1.5 rounded-full bg-emerald-500" />实时更新
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4 text-sm">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-muted-foreground text-xs mb-1">
|
||||
<ArrowDown class="size-3.5 text-emerald-500" />下载
|
||||
</div>
|
||||
<p class="text-lg font-semibold tabular-nums">{{ fmtSpeed(store.traffic?.downloadSpeed ?? 0) }}</p>
|
||||
<p class="text-xs text-muted-foreground tabular-nums">累计 {{ store.traffic ? fmtBytes(store.traffic.downloadTotal) : '--' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-muted-foreground text-xs mb-1">
|
||||
<ArrowUp class="size-3.5 text-rose-500" />上传
|
||||
</div>
|
||||
<p class="text-lg font-semibold tabular-nums">{{ fmtSpeed(store.traffic?.uploadSpeed ?? 0) }}</p>
|
||||
<p class="text-xs text-muted-foreground tabular-nums">累计 {{ store.traffic ? fmtBytes(store.traffic.uploadTotal) : '--' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Waypoints class="size-3.5" />活跃连接
|
||||
</span>
|
||||
<span class="font-semibold tabular-nums">{{ store.traffic?.activeConnections ?? '--' }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<!-- 内核状态 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -1436,6 +1657,139 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 连接 -->
|
||||
<TabsContent value="connections" class="flex-1 mt-4 tab-animate">
|
||||
<div v-if="!running" key="conn-not-running" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10 pr-10">
|
||||
<Waypoints class="size-12 opacity-30" />
|
||||
<p class="text-sm">mihomo 未运行,请先在概览页启动</p>
|
||||
</div>
|
||||
<div v-else class="h-full flex flex-col gap-4 pr-3">
|
||||
<!-- 顶部统计 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<Card>
|
||||
<CardContent class="py-3">
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground text-xs mb-1"><Waypoints class="size-3.5" />活跃连接</div>
|
||||
<p class="text-xl font-semibold tabular-nums">{{ connList.length }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent class="py-3">
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground text-xs mb-1"><ArrowDown class="size-3.5 text-emerald-500" />下载速率</div>
|
||||
<p class="text-xl font-semibold tabular-nums">{{ fmtSpeed(connTotalDownloadSec) }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent class="py-3">
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground text-xs mb-1"><ArrowUp class="size-3.5 text-rose-500" />上传速率</div>
|
||||
<p class="text-xl font-semibold tabular-nums">{{ fmtSpeed(connTotalUploadSec) }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent class="py-3">
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground text-xs mb-1"><Target class="size-3.5" />命中规则</div>
|
||||
<p class="text-xl font-semibold tabular-nums">{{ ruleHitCount }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 规则命中分布 -->
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-sm flex items-center gap-2"><Target class="size-3.5 text-primary" />规则命中</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="pt-0">
|
||||
<div v-if="!ruleHits.length" class="text-xs text-muted-foreground py-2">暂无连接</div>
|
||||
<div v-else class="space-y-1.5">
|
||||
<div v-for="r in ruleHits.slice(0, 6)" :key="r.rule" class="flex items-baseline gap-2 text-xs">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary shrink-0 self-center" />
|
||||
<span class="font-medium shrink-0">{{ translateRule(r.rule) }}</span>
|
||||
<span class="flex-1 min-w-0 truncate text-muted-foreground">
|
||||
<template v-if="ruleDesc(r.rule)">({{ ruleDesc(r.rule) }})</template>
|
||||
</span>
|
||||
<span class="shrink-0 tabular-nums">×{{ r.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 连接列表 -->
|
||||
<Card class="flex-1 min-h-0 flex flex-col">
|
||||
<CardHeader class="pb-2 space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<CardTitle class="text-sm flex items-center gap-2"><Waypoints class="size-3.5 text-primary" />当前连接</CardTitle>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input v-model="connFilter" placeholder="按进程/域名/规则过滤" class="h-8 w-56" />
|
||||
<Button size="xs" variant="outline" :disabled="!connList.length" @click="closeAllConnections">
|
||||
<Square class="size-3" />断开全部
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-xs text-muted-foreground">走向:</span>
|
||||
<Button
|
||||
v-for="s in scopeFilterOptions"
|
||||
:key="s.value"
|
||||
size="xs"
|
||||
:variant="connScopeFilter === s.value ? 'default' : 'outline'"
|
||||
@click="connScopeFilter = s.value"
|
||||
>{{ s.label }}</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-1 min-h-0 overflow-hidden pt-0">
|
||||
<ScrollArea class="h-full">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-card text-muted-foreground">
|
||||
<tr class="border-b">
|
||||
<th class="text-left font-medium py-2 px-2">进程 / 源地址</th>
|
||||
<th class="text-left font-medium py-2 px-2">目标</th>
|
||||
<th class="text-left font-medium py-2 px-2">规则</th>
|
||||
<th class="text-right font-medium py-2 px-2">下载</th>
|
||||
<th class="text-right font-medium py-2 px-2">上传</th>
|
||||
<th class="text-center font-medium py-2 px-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="c in filteredConnections" :key="c.id" class="border-b last:border-0 hover:bg-muted/40">
|
||||
<td class="py-2 px-2 align-baseline">
|
||||
<span class="font-medium truncate block max-w-[140px]">{{ connProcess(c) }}</span>
|
||||
<span class="text-muted-foreground">{{ connSource(c) }}</span>
|
||||
</td>
|
||||
<td class="py-2 px-2 align-baseline">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Badge
|
||||
v-if="connScopeOf(c) === 'direct'"
|
||||
variant="outline" class="h-4 px-1.5 text-[10px] shrink-0 border-emerald-500 text-emerald-500"
|
||||
>国内</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline" class="h-4 px-1.5 text-[10px] shrink-0 border-sky-500 text-sky-500"
|
||||
>国外</Badge>
|
||||
<span class="truncate block max-w-[140px]">{{ connHost(c) }}</span>
|
||||
</div>
|
||||
<span class="text-muted-foreground">{{ c.metadata?.network }} / {{ c.metadata?.type }}</span>
|
||||
</td>
|
||||
<td class="py-2 px-2 align-baseline text-muted-foreground">
|
||||
<span class="truncate block max-w-[160px]">{{ translateRule(c.rule || 'DIRECT') }}</span>
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right tabular-nums align-baseline">{{ fmtBytes(c.download) }}</td>
|
||||
<td class="py-2 px-2 text-right tabular-nums align-baseline">{{ fmtBytes(c.upload) }}</td>
|
||||
<td class="py-2 px-2 text-center align-baseline">
|
||||
<Button size="icon" variant="ghost" class="size-6" title="断开连接" @click="store.closeConnection(c.id)">
|
||||
<X class="size-3.5" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!connList.length">
|
||||
<td colspan="6" class="text-center text-muted-foreground py-8">暂无活跃连接</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 节点 -->
|
||||
<TabsContent value="proxies" class="flex-1 mt-4 tab-animate">
|
||||
<div v-if="!running" key="not-running" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10 pr-10">
|
||||
|
||||
Reference in New Issue
Block a user