feat(admin-frontend): 完成节点与礼品卡管理工作台

补齐节点管理真实新增、编辑与排序流程,接入权限组与路由组
维护页,并支持 11 种协议的动态配置表单

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

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

修复仪表盘 24h 流量排行涨跌始终为 0 的问题,改为对比昨天整日统
计并补充单元测试
This commit is contained in:
yinjianm
2026-04-24 21:58:16 +08:00
parent f7cef30b9c
commit e393b11b61
80 changed files with 8811 additions and 278 deletions
@@ -0,0 +1,410 @@
import type { AdminNodeType } from '@/types/api'
export interface NodeOption<T extends string | number = string> {
value: T
label: string
dotColor?: string
}
export interface NodeRateRangeForm {
key: string
start: string
end: string
rate: number
}
export interface NodeFormModel {
id?: number
originalType: AdminNodeType | ''
type: AdminNodeType | ''
rawProtocolSettings: Record<string, unknown>
name: string
code: string
rate: number
rateTimeEnable: boolean
rateTimeRanges: NodeRateRangeForm[]
tags: string[]
groupIds: number[]
routeIds: number[]
host: string
port: string
serverPort: string
parentId: number | null
show: boolean
enabled: boolean
tlsMode: number
tlsServerName: string
tlsAllowInsecure: boolean
echEnabled: boolean
echConfig: string
echQueryServerName: string
echKey: string
utlsEnabled: boolean
utlsFingerprint: string
realityServerName: string
realityServerPort: string
realityPublicKey: string
realityPrivateKey: string
realityShortId: string
network: string
tcpHeaderType: string
tcpRequestPath: string
tcpRequestHost: string
wsPath: string
wsHost: string
grpcServiceName: string
h2Path: string
h2Host: string
httpupgradePath: string
httpupgradeHost: string
xhttpPath: string
xhttpHost: string
xhttpMode: string
xhttpExtra: string
kcpSeed: string
kcpHeaderType: string
shadowsocksCipher: string
shadowsocksObfs: string
shadowsocksObfsHost: string
shadowsocksObfsPath: string
shadowsocksPlugin: string
shadowsocksPluginOpts: string
vlessFlow: string
vlessEncryptionEnabled: boolean
vlessEncryption: string
vlessDecryption: string
hysteriaVersion: number
hysteriaUpMbps: number | null
hysteriaDownMbps: number | null
hysteriaObfsEnabled: boolean
hysteriaObfsType: string
hysteriaObfsPassword: string
hysteriaHopInterval: number | null
tuicVersion: number | null
tuicCongestionControl: string
tuicAlpn: string[]
tuicUdpRelayMode: string
mieruTransport: string
mieruTrafficPattern: string
anytlsPaddingSchemeText: string
multiplexEnabled: boolean
multiplexProtocol: string
multiplexMaxConnections: number | null
multiplexPadding: boolean
multiplexBrutalEnabled: boolean
multiplexBrutalUpMbps: number | null
multiplexBrutalDownMbps: number | null
}
export const NODE_PROTOCOL_OPTIONS: Array<NodeOption<AdminNodeType>> = [
{ value: 'shadowsocks', label: 'Shadowsocks', dotColor: '#44a35f' },
{ value: 'vmess', label: 'VMess', dotColor: '#d94696' },
{ value: 'trojan', label: 'Trojan', dotColor: '#f3b74f' },
{ value: 'hysteria', label: 'Hysteria', dotColor: '#5d84ff' },
{ value: 'vless', label: 'VLess', dotColor: '#111111' },
{ value: 'tuic', label: 'TUIC', dotColor: '#22c55e' },
{ value: 'socks', label: 'SOCKS', dotColor: '#3b82f6' },
{ value: 'naive', label: 'Naive', dotColor: '#8b3dff' },
{ value: 'http', label: 'HTTP', dotColor: '#ff5c2b' },
{ value: 'mieru', label: 'Mieru', dotColor: '#4caf50' },
{ value: 'anytls', label: 'AnyTLS', dotColor: '#8e59d1' },
]
export const NODE_TLS_MODE_OPTIONS: Array<NodeOption<number>> = [
{ value: 0, label: '无' },
{ value: 1, label: 'TLS' },
{ value: 2, label: 'Reality' },
]
export const NODE_SIMPLE_TLS_OPTIONS: Array<NodeOption<number>> = [
{ value: 0, label: '无' },
{ value: 1, label: 'TLS' },
]
export const NODE_TRANSPORT_OPTIONS: Record<string, Array<NodeOption>> = {
vmess: [
{ value: 'tcp', label: 'TCP' },
{ value: 'ws', label: 'WebSocket' },
{ value: 'grpc', label: 'gRPC' },
{ value: 'h2', label: 'HTTP/2' },
{ value: 'httpupgrade', label: 'HTTPUpgrade' },
{ value: 'xhttp', label: 'XHTTP' },
],
vless: [
{ value: 'tcp', label: 'TCP' },
{ value: 'ws', label: 'WebSocket' },
{ value: 'grpc', label: 'gRPC' },
{ value: 'h2', label: 'HTTP/2' },
{ value: 'httpupgrade', label: 'HTTPUpgrade' },
{ value: 'xhttp', label: 'XHTTP' },
{ value: 'kcp', label: 'mKCP' },
{ value: 'quic', label: 'QUIC' },
],
trojan: [
{ value: 'tcp', label: 'TCP' },
{ value: 'ws', label: 'WebSocket' },
{ value: 'grpc', label: 'gRPC' },
{ value: 'h2', label: 'HTTP/2' },
{ value: 'httpupgrade', label: 'HTTPUpgrade' },
{ value: 'xhttp', label: 'XHTTP' },
],
}
export const NODE_TCP_HEADER_OPTIONS: Array<NodeOption> = [
{ value: 'none', label: '无头部' },
{ value: 'http', label: 'HTTP 伪装' },
]
export const NODE_TLS_FINGERPRINT_OPTIONS: Array<NodeOption> = [
{ value: 'chrome', label: 'Chrome' },
{ value: 'firefox', label: 'Firefox' },
{ value: 'safari', label: 'Safari' },
{ value: 'ios', label: 'iOS' },
{ value: 'edge', label: 'Edge' },
{ value: 'qq', label: 'QQ' },
{ value: 'random', label: '随机' },
]
export const NODE_SHADOWSOCKS_CIPHER_OPTIONS: Array<NodeOption> = [
{ value: 'aes-128-gcm', label: 'aes-128-gcm' },
{ value: 'aes-256-gcm', label: 'aes-256-gcm' },
{ value: 'chacha20-ietf-poly1305', label: 'chacha20-ietf-poly1305' },
{ value: '2022-blake3-aes-128-gcm', label: '2022-blake3-aes-128-gcm' },
{ value: '2022-blake3-aes-256-gcm', label: '2022-blake3-aes-256-gcm' },
{ value: '2022-blake3-chacha20-poly1305', label: '2022-blake3-chacha20-poly1305' },
]
export const NODE_SHADOWSOCKS_OBFS_OPTIONS: Array<NodeOption> = [
{ value: '', label: '无' },
{ value: 'http', label: 'HTTP' },
{ value: 'tls', label: 'TLS' },
]
export const NODE_VLESS_FLOW_OPTIONS: Array<NodeOption> = [
{ value: '', label: '无' },
{ value: 'xtls-rprx-vision', label: 'xtls-rprx-vision' },
{ value: 'xtls-rprx-vision-udp443', label: 'xtls-rprx-vision-udp443' },
]
export const NODE_CONGESTION_CONTROL_OPTIONS: Array<NodeOption> = [
{ value: 'cubic', label: 'cubic' },
{ value: 'bbr', label: 'bbr' },
{ value: 'new_reno', label: 'new_reno' },
]
export const NODE_UDP_RELAY_MODE_OPTIONS: Array<NodeOption> = [
{ value: 'native', label: 'native' },
{ value: 'quic', label: 'quic' },
]
export const NODE_MUX_PROTOCOL_OPTIONS: Array<NodeOption> = [
{ value: 'yamux', label: 'yamux' },
{ value: 'smux', label: 'smux' },
{ value: 'h2mux', label: 'h2mux' },
]
function createRateRange(index = 0): NodeRateRangeForm {
return {
key: `range-${Date.now()}-${index}`,
start: '',
end: '',
rate: 1,
}
}
export function createEmptyNodeForm(): NodeFormModel {
return {
originalType: '',
type: '',
rawProtocolSettings: {},
name: '',
code: '',
rate: 1,
rateTimeEnable: false,
rateTimeRanges: [createRateRange()],
tags: [],
groupIds: [],
routeIds: [],
host: '',
port: '',
serverPort: '',
parentId: null,
show: true,
enabled: true,
tlsMode: 0,
tlsServerName: '',
tlsAllowInsecure: false,
echEnabled: false,
echConfig: '',
echQueryServerName: '',
echKey: '',
utlsEnabled: false,
utlsFingerprint: 'chrome',
realityServerName: '',
realityServerPort: '',
realityPublicKey: '',
realityPrivateKey: '',
realityShortId: '',
network: '',
tcpHeaderType: 'none',
tcpRequestPath: '',
tcpRequestHost: '',
wsPath: '',
wsHost: '',
grpcServiceName: '',
h2Path: '',
h2Host: '',
httpupgradePath: '',
httpupgradeHost: '',
xhttpPath: '',
xhttpHost: '',
xhttpMode: 'auto',
xhttpExtra: '',
kcpSeed: '',
kcpHeaderType: 'none',
shadowsocksCipher: '2022-blake3-aes-128-gcm',
shadowsocksObfs: '',
shadowsocksObfsHost: '',
shadowsocksObfsPath: '',
shadowsocksPlugin: '',
shadowsocksPluginOpts: '',
vlessFlow: '',
vlessEncryptionEnabled: false,
vlessEncryption: '',
vlessDecryption: '',
hysteriaVersion: 2,
hysteriaUpMbps: null,
hysteriaDownMbps: null,
hysteriaObfsEnabled: false,
hysteriaObfsType: 'salamander',
hysteriaObfsPassword: '',
hysteriaHopInterval: null,
tuicVersion: 5,
tuicCongestionControl: 'cubic',
tuicAlpn: ['h3'],
tuicUdpRelayMode: 'native',
mieruTransport: 'TCP',
mieruTrafficPattern: '',
anytlsPaddingSchemeText: [
'stop=8',
'0=30-30',
'1=100-400',
'2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000',
].join('\n'),
multiplexEnabled: false,
multiplexProtocol: 'yamux',
multiplexMaxConnections: null,
multiplexPadding: false,
multiplexBrutalEnabled: false,
multiplexBrutalUpMbps: null,
multiplexBrutalDownMbps: null,
}
}
export function createNodeRateRange(): NodeRateRangeForm {
return createRateRange()
}
export function getNodeProtocolLabel(type: AdminNodeType | '' | string): string {
return NODE_PROTOCOL_OPTIONS.find((item) => item.value === type)?.label ?? String(type ?? '')
}
export function getNodeProtocolOptions(): Array<NodeOption<AdminNodeType>> {
return NODE_PROTOCOL_OPTIONS
}
export function getNodeTlsOptions(type: AdminNodeType | '' | string): Array<NodeOption<number>> {
return type === 'vless' || type === 'trojan'
? NODE_TLS_MODE_OPTIONS
: NODE_SIMPLE_TLS_OPTIONS
}
export function getNodeTransportOptions(type: AdminNodeType | '' | string): Array<NodeOption> {
return NODE_TRANSPORT_OPTIONS[type] ?? []
}
export function supportsNodeSecurity(type: AdminNodeType | '' | string): boolean {
return ['vmess', 'vless', 'trojan', 'hysteria', 'tuic', 'anytls', 'socks', 'naive', 'http'].includes(type)
}
export function supportsNodeTransport(type: AdminNodeType | '' | string): boolean {
return ['vmess', 'vless', 'trojan'].includes(type)
}
export function supportsNodeMultiplex(type: AdminNodeType | '' | string): boolean {
return ['vmess', 'vless', 'trojan', 'mieru'].includes(type)
}
export function shouldShowTlsSettings(type: AdminNodeType | '' | string, tlsMode: number): boolean {
if (['hysteria', 'tuic', 'anytls'].includes(type)) {
return true
}
if (['vmess', 'socks', 'naive', 'http'].includes(type)) {
return tlsMode === 1
}
if (['vless', 'trojan'].includes(type)) {
return tlsMode === 1
}
return false
}
export function shouldShowRealitySettings(type: AdminNodeType | '' | string, tlsMode: number): boolean {
return ['vless', 'trojan'].includes(type) && tlsMode === 2
}
export function getNodeProtocolHint(type: AdminNodeType | '' | string): string {
const hints: Record<string, string> = {
shadowsocks: '配置 cipher、混淆与 plugin,适合传统 SS 节点维护。',
vmess: '配置 TLS 与传输层参数,适合 VMess 客户端场景。',
trojan: '配置 TLS / Reality 与传输层,适合 Trojan 高兼容场景。',
hysteria: '配置版本、带宽、混淆与 TLS 信息。',
vless: '配置安全性、传输协议、Flow、Reality 与加密模式。',
tuic: '配置版本、拥塞控制、ALPN 与 UDP relay。',
socks: '配置基础 SOCKS 节点,支持可选 TLS。',
naive: '配置 NaiveProxy 基础 TLS 信息。',
http: '配置 HTTP 节点与可选 TLS。',
mieru: '配置传输方式、流量模式与多路复用。',
anytls: '配置 AnyTLS 的 TLS 信息与 Padding Scheme。',
}
return hints[type] ?? '请选择协议后继续配置。'
}
export function validateNodeForm(form: NodeFormModel): string | null {
if (!form.type) {
return '请选择协议类型'
}
if (form.rateTimeEnable) {
const validRanges = form.rateTimeRanges.filter((item) => item.start.trim() && item.end.trim() && Number(item.rate) > 0)
if (validRanges.length === 0) {
return '请至少填写一条有效的动态倍率规则'
}
}
if (form.type === 'shadowsocks' && !form.shadowsocksCipher.trim()) {
return '请选择 Shadowsocks 加密方式'
}
if (['vmess', 'trojan', 'vless'].includes(form.type) && !form.network.trim()) {
return '请选择传输协议'
}
if (['vmess', 'socks', 'naive', 'http'].includes(form.type) && form.tlsMode === 1 && !form.tlsServerName.trim()) {
return '启用 TLS 时请输入服务器名称(SNI)'
}
if (['vless', 'trojan'].includes(form.type) && form.tlsMode === 2) {
if (!form.realityServerName.trim()) return 'Reality 模式下请输入服务器名称'
if (!form.realityPublicKey.trim()) return 'Reality 模式下请输入公钥'
if (!form.realityShortId.trim()) return 'Reality 模式下请输入 Short ID'
}
if (form.network === 'xhttp' && form.xhttpExtra.trim()) {
try {
JSON.parse(form.xhttpExtra)
} catch {
return 'XHTTP 额外参数必须是合法 JSON'
}
}
if (form.type === 'hysteria' && form.hysteriaObfsEnabled && !form.hysteriaObfsPassword.trim()) {
return '启用 Hysteria 混淆时请输入混淆密码'
}
if (form.type === 'tuic' && form.tuicAlpn.length === 0) {
return '请至少保留一个 TUIC ALPN'
}
return null
}