连接页:每秒轮询的连接表格,能按入站看、能断开

/connections 没有流式版本,只能全量轮询;断开是运行时动作,不走
落盘那条保护链路。两个容易做反的地方都收在 shared/connections.ts:
chains 数组第一个是最终出口,显示要倒过来;没有连接时 connections
字段是 null 而不是空数组。虚拟滚动用 @tanstack/vue-virtual,ADR 0006
留的两个例外之一,它只有 virtual-core 一个运行时依赖。
This commit is contained in:
2026-08-11 18:48:55 +08:00
parent 6c8a1c4e0e
commit c9a0b87af4
6 changed files with 932 additions and 174 deletions
+171
View File
@@ -0,0 +1,171 @@
import { describe, expect, it } from 'bun:test'
import {
chainLabel,
filterConnections,
formatDuration,
groupByInbound,
hostLabel,
ruleLabel,
sortConnections,
} from './connections.ts'
import type { ConnectionItem, ConnectionMeta } from './connections.ts'
const NOW = Date.parse('2026-08-11T18:40:00+08:00')
function meta(over: Partial<ConnectionMeta> = {}): ConnectionMeta {
return {
host: 'example.com',
destinationIP: '93.184.216.34',
destinationPort: '443',
inboundName: 'lxc-web',
inboundPort: '17891',
...over,
}
}
function conn(over: Partial<ConnectionItem> = {}): ConnectionItem {
return {
id: 'a',
upload: 0,
download: 0,
start: '2026-08-11T18:39:00+08:00',
chains: ['[vless]某线路', 'AUTO', 'PROXY'],
rule: 'DomainSuffix',
rulePayload: 'example.com',
metadata: meta(),
...over,
}
}
describe('主机标签', () => {
it('有域名用域名,带端口', () => {
expect(hostLabel(meta())).toBe('example.com:443')
})
it('没域名退回 IP', () => {
expect(hostLabel(meta({ host: '' }))).toBe('93.184.216.34:443')
})
it('没端口就只给主机', () => {
expect(hostLabel(meta({ destinationPort: '' }))).toBe('example.com')
})
})
describe('出站链', () => {
it('倒过来显示:最先接触的在左,最终出口在右', () => {
expect(chainLabel(['[vless]某线路', 'AUTO', 'PROXY'])).toBe('PROXY → AUTO → [vless]某线路')
})
it('单跳就是直连', () => {
expect(chainLabel(['DIRECT'])).toBe('DIRECT')
})
})
describe('规则标签', () => {
it('类型和载荷用空格连起来', () => {
expect(ruleLabel('DomainSuffix', 'example.com')).toBe('DomainSuffix example.com')
})
it('兜底规则没有载荷', () => {
expect(ruleLabel('Match', '')).toBe('Match')
})
})
describe('持续时间', () => {
it('不满一分钟按秒', () => {
expect(formatDuration('2026-08-11T18:39:18+08:00', NOW)).toBe('42 秒')
})
it('满一分钟带分', () => {
expect(formatDuration('2026-08-11T18:36:36+08:00', NOW)).toBe('3 分 24 秒')
})
it('满一小时不再给秒', () => {
expect(formatDuration('2026-08-11T16:35:00+08:00', NOW)).toBe('2 小时 5 分')
})
it('start 带时区偏移,跨时区解析不能算错', () => {
// 同一时刻写成 UTC 和 +08:00,算出来的持续时间必须一样
const local = formatDuration('2026-08-11T18:39:00+08:00', NOW)
const utc = formatDuration('2026-08-11T10:39:00Z', NOW)
expect(local).toBe(utc)
})
it('坏数据返回空串,未来的时间按 0 算', () => {
expect(formatDuration('not-a-date', NOW)).toBe('')
expect(formatDuration('2026-08-11T19:00:00+08:00', NOW)).toBe('0 秒')
})
})
describe('排序', () => {
const list = [
conn({ id: 'small-new', download: 100, start: '2026-08-11T18:39:50+08:00' }),
conn({ id: 'big-new', download: 9000, start: '2026-08-11T18:39:40+08:00' }),
conn({ id: 'small-old', download: 50, start: '2026-08-11T18:00:00+08:00' }),
]
it('按下载量降序', () => {
expect(sortConnections(list, 'download', NOW).map((c) => c.id)).toEqual([
'big-new',
'small-new',
'small-old',
])
})
it('按持续时间降序:活得久的排前面', () => {
expect(sortConnections(list, 'duration', NOW).map((c) => c.id)).toEqual([
'small-old',
'big-new',
'small-new',
])
})
it('不动原数组', () => {
const before = list.map((c) => c.id)
sortConnections(list, 'download', NOW)
expect(list.map((c) => c.id)).toEqual(before)
})
})
describe('筛选', () => {
const list = [
conn({ id: 'a', metadata: meta({ host: 'speed.cloudflare.com' }) }),
conn({ id: 'b', metadata: meta({ host: '', destinationIP: '1.2.3.4' }) }),
]
it('匹配主机名,大小写不敏感', () => {
expect(filterConnections(list, 'CLOUDFLARE').map((c) => c.id)).toEqual(['a'])
})
it('没域名时也能按 IP 搜', () => {
expect(filterConnections(list, '1.2').map((c) => c.id)).toEqual(['b'])
})
it('空串不筛,空白也不算条件', () => {
expect(filterConnections(list, ' ')).toHaveLength(2)
})
})
describe('按入站聚合', () => {
it('同一路入站计数、流量都累加,连接多的排前面', () => {
const list = [
conn({ id: 'a', download: 10, upload: 1 }),
conn({ id: 'b', download: 20, upload: 2 }),
conn({ id: 'c', metadata: meta({ inboundName: 'lxc-db', inboundPort: '17892' }) }),
]
expect(groupByInbound(list)).toEqual([
{ label: 'lxc-web(17891)', count: 2, upload: 3, download: 30 },
{ label: 'lxc-db(17892)', count: 1, upload: 0, download: 0 },
])
})
it('默认入站用内核给的内部名,附上端口', () => {
const list = [conn({ metadata: meta({ inboundName: 'DEFAULT-MIXED', inboundPort: '17890' }) })]
expect(groupByInbound(list)[0]?.label).toBe('DEFAULT-MIXED(17890)')
})
it('入站名字缺失时用端口凑', () => {
const list = [conn({ metadata: meta({ inboundName: '' }) })]
expect(groupByInbound(list)[0]?.label).toBe('17891')
})
})