Files
Xboard/admin-frontend/src/utils/nodes.ts
T
yinjianm e393b11b61 feat(admin-frontend): 完成节点与礼品卡管理工作台
补齐节点管理真实新增、编辑与排序流程,接入权限组与路由组
维护页,并支持 11 种协议的动态配置表单

开放礼品卡管理入口,交付模板、兑换码、使用记录与统计四页签
工作台,接入 gift-card 相关后台接口

将知识库、权限组与路由管理从占位页升级为真实页面,并修复侧边栏
低高度裁切问题

修复仪表盘 24h 流量排行涨跌始终为 0 的问题,改为对比昨天整日统
计并补充单元测试
2026-04-24 21:58:16 +08:00

152 lines
3.8 KiB
TypeScript

import type { AdminNodeItem } from '@/types/api'
export interface NodeStatusMeta {
label: string
dotClass: 'online' | 'pending' | 'offline' | 'disabled'
tagType: 'success' | 'warning' | 'danger' | 'info'
}
const NODE_TYPE_LABELS: Record<string, string> = {
shadowsocks: 'Shadowsocks',
trojan: 'Trojan',
vmess: 'VMess',
vless: 'VLess',
hysteria: 'Hysteria',
tuic: 'TUIC',
anytls: 'AnyTLS',
socks: 'SOCKS',
http: 'HTTP',
naive: 'Naive',
mieru: 'Mieru',
}
function normalizeText(value: unknown): string {
return String(value ?? '').trim().toLowerCase()
}
export function getNodeTypeLabel(type: string): string {
const normalized = normalizeText(type)
return NODE_TYPE_LABELS[normalized] ?? String(type || '未知协议').toUpperCase()
}
export function getNodeStatusMeta(node: AdminNodeItem): NodeStatusMeta {
if (node.enabled === false) {
return {
label: '已停用',
dotClass: 'disabled',
tagType: 'info',
}
}
if (node.available_status === 2) {
return {
label: '在线',
dotClass: 'online',
tagType: 'success',
}
}
if (node.available_status === 1) {
return {
label: '待同步',
dotClass: 'pending',
tagType: 'warning',
}
}
return {
label: '离线',
dotClass: 'offline',
tagType: 'danger',
}
}
export function getNodeIdLabel(node: AdminNodeItem): string {
return node.parent_id ? `${node.id}${node.parent_id}` : String(node.id)
}
export function getNodeAddress(node: AdminNodeItem): { primary: string; secondary: string } {
const host = node.host || '--'
const publicPort = node.server_port ?? node.port ?? '--'
const innerPort = node.port ?? node.server_port ?? '--'
return {
primary: `${host}:${publicPort}`,
secondary: `内部端口 ${innerPort}`,
}
}
export function formatNodeRate(rate?: number | null): string {
const normalized = Number.isFinite(Number(rate)) ? Number(rate) : 1
return `${normalized.toFixed(2)} x`
}
export function getNodeGroupNames(node: AdminNodeItem): string[] {
return (node.groups ?? [])
.map((group) => group.name)
.filter(Boolean)
}
export function buildNodeTypeOptions(nodes: AdminNodeItem[]): Array<{ label: string; value: string }> {
const uniqueTypes = [...new Set(nodes.map((node) => normalizeText(node.type)).filter(Boolean))]
return uniqueTypes.map((value) => ({
value,
label: getNodeTypeLabel(value),
}))
}
function buildNodeSearchText(node: AdminNodeItem): string {
return [
node.id,
node.parent_id,
node.name,
node.host,
node.port,
node.server_port,
getNodeTypeLabel(node.type),
...getNodeGroupNames(node),
]
.map((item) => String(item ?? '').trim())
.filter(Boolean)
.join(' ')
.toLowerCase()
}
export function filterNodes(
nodes: AdminNodeItem[],
keyword: string,
typeFilter: string,
groupFilter: string,
): AdminNodeItem[] {
const normalizedKeyword = normalizeText(keyword)
const normalizedType = normalizeText(typeFilter)
const normalizedGroup = normalizeText(groupFilter)
return nodes.filter((node) => {
if (normalizedKeyword && !buildNodeSearchText(node).includes(normalizedKeyword)) {
return false
}
if (normalizedType !== '' && normalizedType !== 'all' && normalizeText(node.type) !== normalizedType) {
return false
}
if (normalizedGroup !== '' && normalizedGroup !== 'all') {
const belongsToGroup = (node.groups ?? []).some((group) => String(group.id) === normalizedGroup)
if (!belongsToGroup) {
return false
}
}
return true
})
}
export function countOnlineNodes(nodes: AdminNodeItem[]): number {
return nodes.filter((node) => getNodeStatusMeta(node).dotClass === 'online').length
}
export function countVisibleNodes(nodes: AdminNodeItem[]): number {
return nodes.filter((node) => Boolean(node.show)).length
}