feat:接口联调试完毕

This commit is contained in:
liyuanhu 2025-04-22 16:04:37 +08:00
parent 5000c88f0f
commit fae2e8e7ec
31 changed files with 735 additions and 1566 deletions

6
.env
View File

@ -1,8 +1,8 @@
IS_DEBUG="true"
GRPC_ENDPOINT="10.66.66.234:9090"
COSMOS_ENDPOINT="http://10.66.66.234:26657"
NODE_SECRET="aHVnZSBjb21wYW55IHBob25lIHdlc3QgcGxhY2Ugc2VtaW5hciBtaXJhY2xlIGxlbmQgbWFuZGF0ZSB0aGVuIGFkanVzdCBxdWl0IG1lYXQgY2hlYXAgbm9vZGxlIGNvdXBsZSBkZWZpbmUgbXVzY2xlIHB1bHNlIHNpc3RlciBwaWVjZSBkZXZpY2UgcHJpdmF0ZSBob29k"
IS_DEBUG="true"
ACCOUNT_NAME="de1"
ACCOUNT_NAME="ccvdexre"
NODE_SECRET= "initial typical business width enforce buddy magic country piano head cable blossom gate caught disagree pepper moral pair vessel protect mixture deposit artwork liquid"
VIET_EVENTS_URL="ws://10.66.66.234:8080/events"
VITE_BASE_URL="http://10.66.66.234:6060"
VITE_BLOCK_URL="http://10.66.66.234:1317"

View File

@ -29,6 +29,7 @@
"@tanstack/react-table": "^8.20.6",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-fs": "~2.2.1",
"@tauri-apps/plugin-global-shortcut": "~2",
"@tauri-apps/plugin-http": "~2.2.0",
"@tauri-apps/plugin-os": "~2",
@ -61,6 +62,7 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.4",
"xlsx": "^0.18.5",
"zod": "^3.24.1",
"zustand": "^5.0.2"
},

82
pnpm-lock.yaml generated
View File

@ -62,6 +62,9 @@ importers:
'@tauri-apps/plugin-dialog':
specifier: ~2
version: 2.2.0
'@tauri-apps/plugin-fs':
specifier: ~2.2.1
version: 2.2.1
'@tauri-apps/plugin-global-shortcut':
specifier: ~2
version: 2.2.0
@ -158,6 +161,9 @@ importers:
uuid:
specifier: ^11.0.4
version: 11.0.5
xlsx:
specifier: ^0.18.5
version: 0.18.5
zod:
specifier: ^3.24.1
version: 3.24.1
@ -1523,6 +1529,9 @@ packages:
'@tauri-apps/plugin-dialog@2.2.0':
resolution: {integrity: sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==}
'@tauri-apps/plugin-fs@2.2.1':
resolution: {integrity: sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==}
'@tauri-apps/plugin-global-shortcut@2.2.0':
resolution: {integrity: sha512-clI9Bg/BcxWXNDK+ij601o1qC2WxMEy8ovhGgEW5Ai17oPy0KK8uwzmc59KiVnOYKpBWHCUPqBxG+KBNUFXgzw==}
@ -1606,6 +1615,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
ahooks@3.8.4:
resolution: {integrity: sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==}
engines: {node: '>=8.0.0'}
@ -1701,6 +1714,10 @@ packages:
caniuse-lite@1.0.30001684:
resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chalk@4.1.1:
resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==}
engines: {node: '>=10'}
@ -1735,6 +1752,10 @@ packages:
code-inspector-plugin@0.19.1:
resolution: {integrity: sha512-WtgGT3Ky5QUtv34eEXf57oIa15yLseVTtV1ngELxHgpg46ebjvaROpGOLtOEtyu0EB+RhLJ2Mj6hEnLe3UCcLg==}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -1771,6 +1792,11 @@ packages:
typescript:
optional: true
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -1883,6 +1909,10 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@ -2675,6 +2705,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
string-convert@0.2.1:
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
@ -2885,6 +2919,14 @@ packages:
engines: {node: '>= 8'}
hasBin: true
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -2893,6 +2935,11 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -4151,6 +4198,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-fs@2.2.1':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-global-shortcut@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
@ -4257,6 +4308,8 @@ snapshots:
acorn@8.14.0: {}
adler-32@1.3.1: {}
ahooks@3.8.4(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
@ -4398,6 +4451,11 @@ snapshots:
caniuse-lite@1.0.30001684: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chalk@4.1.1:
dependencies:
ansi-styles: 4.3.0
@ -4458,6 +4516,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
codepage@1.15.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -4487,6 +4547,8 @@ snapshots:
optionalDependencies:
typescript: 5.7.2
crc-32@1.2.2: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -4605,6 +4667,8 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
frac@1.1.2: {}
fraction.js@4.3.7: {}
fsevents@2.3.3:
@ -5424,6 +5488,10 @@ snapshots:
source-map-js@1.2.1: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
string-convert@0.2.1: {}
string-width@4.2.3:
@ -5644,6 +5712,10 @@ snapshots:
dependencies:
isexe: 2.0.0
wmf@1.0.2: {}
word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@ -5656,6 +5728,16 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
yallist@3.1.1: {}
yaml@2.6.1: {}

1
src-tauri/Cargo.lock generated
View File

@ -2960,6 +2960,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-http",
"tauri-plugin-os",

View File

@ -69,6 +69,7 @@ tauri-plugin-process = "2"
tauri-plugin-http = "2"
tauri-plugin-store = "2"
tauri-plugin-websocket = "2"
tauri-plugin-fs = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"

View File

@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:default",
"core:window:allow-close",
@ -55,6 +57,16 @@
},
"store:default",
"websocket:default",
"http:default"
"http:default",
"fs:default",
{
"identifier": "fs:allow-exists",
"allow": [
{
"path": "$APPDATA/*"
}
]
},
"fs:default"
]
}
}

View File

@ -36,6 +36,14 @@ pub async fn get_nodes(app: AppHandle) -> CommandResult<Vec<api::ProxyNodeInfo>>
cmd_result(core::api::get_nodes(config).await)
}
/// 核心:获取节点列表,可测试核心是否可用
#[tauri::command]
#[specta::specta]
pub async fn get_nodes_update(app: AppHandle) -> CommandResult<Vec<api::ProxyNodeInfo>> {
let config = cmd_result(get_config(&app))?;
cmd_result(core::api::get_nodes_update(config).await)
}
/// 核心:开启代理
#[tauri::command]
#[specta::specta]

View File

@ -12,7 +12,6 @@ use paw_common::preclude::{CoreConfig, CoreResponse};
#[derive(Debug, Clone, Deserialize, Serialize, specta::Type)]
pub struct ProxyNodeInfo {
pub name: String,
pub ip: String,
pub country_code: String,
pub country_name: String,
pub country_name_zh: String,
@ -60,6 +59,28 @@ pub fn config_header(config: CoreConfig) -> reqwest::header::HeaderMap {
headers
}
/// 获取所有可用节点信息,可以用来当作测试健康度的函数
///
/// get /nodes
pub async fn get_nodes_update(config: CoreConfig) -> Result<Vec<ProxyNodeInfo>> {
let client = reqwest::Client::new();
let response = client
.get(format!("{}/nodes?update=true", config.core_api_url()))
.headers(config_header(config))
.timeout(Duration::from_millis(100)) // 由于正常情况下节点获取速度很快而且是本地API为了加快检查核心是否正常设置较短的超时
.send()
.await?;
if response.status().is_success() {
let result: GetNodesResponse = response.json().await?;
debug!("Successfully got nodes: {:?}", result.data);
Ok(result.data)
} else {
debug!("Failed to get nodes: {}", response.text().await?);
bail!("Failed to get nodes")
}
}
/// 获取所有可用节点信息,可以用来当作测试健康度的函数
///
/// get /nodes

View File

@ -17,6 +17,7 @@ pub fn run() -> Result<()> {
// 在这里注册所有需要用的 command
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
cmds::get_nodes,
cmds::get_nodes_update,
cmds::enable_proxy,
cmds::disable_proxy,
cmds::select_node,
@ -54,6 +55,7 @@ pub fn run() -> Result<()> {
// 构建 tauri 程序
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_http::init())

View File

@ -5,7 +5,6 @@
"build": {
"beforeDevCommand": "pnpm web:dev",
"devUrl": "http://127.0.0.1:1420",
"beforeBuildCommand": "pnpm web:build",
"frontendDist": "../dist"
},
"app": {

View File

@ -1,10 +1,11 @@
import { useEffect, useRef } from "react";
import dayjs from "dayjs";
import { useStartupCheck } from "@/hooks/useStartupCheck";
import { usePreventDefault } from "@/hooks/usePreventDefaultEventListener";
import { useGlobalShortcut } from "@/hooks/useGlobalShortcut";
import { useRecreateTheCircuit } from "@/hooks/useRecreateTheCircuit";
import { useCoreConfig } from "@/hooks/useCoreConfig";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import {
setMaliciousNodeList,
setNodeDownList,
@ -12,16 +13,13 @@ import {
setWeb3List2,
} from "@/store/web3Slice";
import type { AppDispatch } from "@/store";
import eventBus, { eventTypes } from "@/utils/eventBus";
import { WebSocketClient } from "@/utils/webSocketClient";
import { WebSocketClient } from "@/utils/webSocketClientV2";
import { blockChainApi } from "@/api/block";
import { getRandomCountryKey } from "@/data";
import Titlebar from "@/components/Titlebar";
import Layout from "@/layout";
import Tray from "@/components/Tray";
import { api } from "./utils/api";
function App() {
// 执行启动自检
@ -35,178 +33,289 @@ function App() {
// 读取配置,若文件不存在则持久化到本地 `%APPDATA%/com.paw.paw-gui`
const { loadCoreConfig } = useCoreConfig();
loadCoreConfig();
const dispatch = useDispatch<AppDispatch>();
// const { } = useSelector(
// (state: RootState) => state.web3Reducer
// );
let eventsWs: WebSocketClient | null = null;
const openWsTraffic = async () => {
if (eventsWs) return;
eventsWs = new WebSocketClient("ws://10.66.66.234:8080/events", {});
console.log(eventsWs, "openWsTraffic Start");
// 执行 WebSocket 操作
await eventsWs?.connect();
eventsWs?.addListener((msg: any) => {
try {
const msgData = msg ? JSON.parse(msg.data) : {};
if (msgData.code === 0) {
// console.log(msgData, "msgDatamsgData");
switch (msgData.event) {
case eventTypes.NODE_UP:
console.log("节点上线");
break;
case eventTypes.NODE_DOWN:
// 添加下线节点到store 里面
if (msgData.data.name) {
// 获取一个随机的国家code
const countryCode = getRandomCountryKey();
dispatch(
setNodeDownList({
name: msgData.data.name,
code: countryCode,
})
);
}
console.log("节点下线");
break;
case eventTypes.MALICIOUS_NODE:
// 添加恶意节点到store 里面
if (msgData.data.name) {
// 获取一个随机的国家code
const countryCode = getRandomCountryKey();
dispatch(
setMaliciousNodeList({
name: msgData.data.name,
code: countryCode,
})
);
}
console.log("检测到恶意节点");
break;
case eventTypes.NODE_INIT_COMPLATE:
console.log("节点预配置完成");
break;
case eventTypes.NODE_REMOVE:
console.log("节点清除");
break;
case eventTypes.NODE_ADD:
console.log("添加节点");
break;
case eventTypes.NODE_INIT:
console.log("节点预配置");
break;
default:
break;
}
const eventsWsRef = useRef<WebSocketClient | null>(null);
const blockchainTimerRef = useRef<NodeJS.Timeout | null>(null);
const wsInitializedRef = useRef<boolean>(false);
// 处理 WebSocket 消息
const handleWebSocketMessage = (msg: any) => {
try {
const msgData = msg ? JSON.parse(msg.data) : {};
console.log("Received WebSocket message:", msgData);
if (msgData?.code === 0) {
switch (msgData.event) {
case eventTypes.NODE_UP:
console.log("节点上线");
break;
case eventTypes.NODE_DOWN:
// 添加下线节点到store 里面
if (msgData.data.name) {
// 获取一个随机的国家code
const countryCode = getRandomCountryKey();
dispatch(
setNodeDownList({
name: msgData.data.name,
code: countryCode,
})
);
}
console.log("节点下线");
break;
case eventTypes.MALICIOUS_NODE:
// 添加恶意节点到store 里面
if (msgData.data.name) {
// 获取一个随机的国家code
const countryCode = getRandomCountryKey();
dispatch(
setMaliciousNodeList({
name: msgData.data.name,
code: countryCode,
})
);
}
console.log("检测到恶意节点");
break;
case eventTypes.NODE_INIT_COMPLATE:
if (eventsWsRef.current?.isWebSocketConnected()) {
eventsWsRef.current.sendMessage(
JSON.stringify({
...msgData,
event: eventTypes.NODE_ADD,
})
);
console.log("节点预配置完成:", {
...msgData,
event: eventTypes.NODE_ADD,
});
}
break;
case eventTypes.NODE_REMOVE:
console.log("节点清除");
break;
case eventTypes.NODE_ADD:
console.log("添加节点");
break;
default:
break;
}
} catch (error) {
console.log(error, "error");
}
});
console.log(eventsWs, "openWsTraffic End");
};
const createdSoketEventBus = async () => {
eventBus.on(eventTypes.NODE_INIT_COMPLATE, (data: any) => {
console.log("节点预配置完成", data);
eventsWs?.sendMessage(data);
});
eventBus.on(eventTypes.NODE_REMOVE, (data: any) => {
console.log("节点清除");
const timestamp = dayjs().unix();
const params = {
code: 0,
event: eventTypes.NODE_REMOVE,
data: {
name: data,
},
timestamp,
};
console.log(JSON.stringify(params), "节点清除 params");
eventsWs?.sendMessage(JSON.stringify(params));
});
};
const closeWsTraffic = async () => {
if (eventsWs) {
await eventsWs.disconnect();
eventsWs = null;
} catch (error) {
console.error("Error processing WebSocket message:", error);
}
};
const removeSoketEventBus = () => {
eventBus.off(eventTypes.NODE_INIT_COMPLATE);
// 初始化 WebSocket 连接
const initWebSocket = async () => {
if (eventsWsRef.current) return;
console.log("Initializing WebSocket connection...");
eventsWsRef.current = new WebSocketClient(
"ws://10.66.66.234:8080/events",
{},
{
autoReconnect: true,
maxReconnectAttempts: 10,
reconnectDelay: 2000,
}
);
const connected = await eventsWsRef.current.connect();
if (connected) {
console.log("WebSocket connected successfully");
eventsWsRef.current.addListener(handleWebSocketMessage);
wsInitializedRef.current = true;
} else {
console.error("Failed to establish WebSocket connection");
}
};
// 设置事件总线监听器
const setupEventBusListeners = () => {
console.log("执行了没")
// 添加节点
eventBus.on(eventTypes.NODE_ADD, (data: any) => {
console.log("添加节点", data);
if (eventsWsRef.current?.isWebSocketConnected()) {
const timestamp = dayjs().unix();
const params = {
code: 0,
event: eventTypes.NODE_ADD,
data: data,
timestamp,
};
eventsWsRef.current.sendMessage(JSON.stringify(params));
} else {
console.warn("WebSocket not connected, can't send NODE_ADD message");
}
});
// 节点预配置事件
eventBus.on(eventTypes.NODE_INIT, (data: any) => {
console.log("节点预配置", data);
if (eventsWsRef.current?.isWebSocketConnected()) {
const timestamp = dayjs().unix();
const params = {
code: 0,
event: "node_init",
data: {},
timestamp: timestamp,
};
eventsWsRef.current.sendMessage(JSON.stringify(params));
} else {
console.warn("WebSocket not connected, can't send NODE_INIT message");
}
});
// 节点清除事件
eventBus.on(eventTypes.NODE_REMOVE, (data: any) => {
console.log("节点清除", data);
if (eventsWsRef.current?.isWebSocketConnected()) {
const timestamp = dayjs().unix();
const params = {
code: 0,
event: eventTypes.NODE_REMOVE,
data: {
name: data,
},
timestamp,
};
eventsWsRef.current.sendMessage(JSON.stringify(params));
} else {
console.warn("WebSocket not connected, can't send NODE_REMOVE message");
}
});
};
// 清理事件总线监听器
const cleanupEventBusListeners = () => {
eventBus.off(eventTypes.NODE_INIT);
eventBus.off(eventTypes.NODE_REMOVE);
};
const timer = useRef<NodeJS.Timeout | null>(null);
// 启动区块链数据轮询
const startBlockchainPolling = () => {
// 立即执行一次
fetchBlockchainData();
const initWebsocketsAndEventBus = async () => {
await openWsTraffic();
await createdSoketEventBus();
timer.current = setInterval(() => {
blockChainApi.getLatestBlock().then((res) => {
blockChainApi
.getTxsByBlock(res.data.block.last_commit.height)
.then((res) => {
const height = res.data.block.last_commit.height;
const timer = res.data.block.header.time;
const txs = res.data.txs;
const balance = res.data.txs.reduce((acc: any, item: any) => {
const blance =
item.auth_info.fee.gas_limit *
Number(blockChainApi.blockConfig.minimum_gas_price);
return acc + blance;
}, 0)
const item = {
height: height,
txs: txs,
timerstamp: dayjs(timer).format("HH:mm:ss"),
balance,
// 保留三位小数
balanceToFixed: Number(balance).toFixed(3),
};
// const timerstamp = dayjs().unix();
dispatch(setWeb3List(item));
console.log(item, "getTxsByBlock");
});
});
}, 5000);
// 设置定时器定期获取数据
blockchainTimerRef.current = setInterval(fetchBlockchainData, 5000);
};
// 获取区块链数据
const fetchBlockchainData = async () => {
try {
const blockRes = await blockChainApi.getLatestBlock();
const height = blockRes.data.block.last_commit.height;
const txsRes = await blockChainApi.getTxsByBlock(height);
const timer = txsRes.data.block.header.time;
const txs = txsRes.data.txs;
const balance = txs.reduce((acc: number, item: any) => {
const blance =
item.auth_info.fee.gas_limit *
Number(blockChainApi.blockConfig.minimum_gas_price);
return acc + blance;
}, 0);
const item = {
height: height,
txs: txs,
timerstamp: dayjs(timer).format("HH:mm:ss"),
balance,
balanceToFixed: Number(balance).toFixed(3),
};
dispatch(setWeb3List(item));
} catch (error) {
console.error("Error fetching blockchain data:", error);
}
};
// 初始化所有服务
const initializeServices = async () => {
// 初始化示例数据
dispatch(
setWeb3List2([
{
id: "6",
name: "Cardano Wallet",
payType: "ADA",
status: "active",
createdAt: 1737436420,
upDatedAt: 5,
balance: "65",
address:
"addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f",
privateKey:
"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
publicKey:
"addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f",
numberTransactions: "35",
transactions: 1,
txs: [],
height: 1204,
timerstamp: dayjs().format("HH:mm:ss"),
balanceToFixed: 0,
},
])
);
// 初始化 WebSocket
await initWebSocket();
// 设置事件总线
setupEventBusListeners();
// 启动区块链数据轮询
startBlockchainPolling();
};
// 清理所有资源
const cleanupServices = () => {
// 清理区块链数据轮询
if (blockchainTimerRef.current) {
clearInterval(blockchainTimerRef.current);
blockchainTimerRef.current = null;
}
// 清理事件总线
cleanupEventBusListeners();
// 关闭 WebSocket 连接
if (eventsWsRef.current) {
eventsWsRef.current.disconnect();
eventsWsRef.current = null;
}
wsInitializedRef.current = false;
};
useEffect(() => {
dispatch(setWeb3List2([
{
id: "6",
name: "Cardano Wallet",
payType: "ADA",
status: "active",
createdAt: 1737436420,
upDatedAt: 5,
balance: "65",
address:
"addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f",
privateKey:
"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
publicKey:
"addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f",
numberTransactions: "35",
transactions: 1,
txs: [],
height: 0,
timerstamp: dayjs().format("HH:mm:ss"),
balanceToFixed: 0,
},
]))
setTimeout(() => {
initWebsocketsAndEventBus();
// 延迟初始化以确保组件完全挂载
const initTimer = setTimeout(() => {
initializeServices();
}, 1000);
// 组件卸载时清理资源
return () => {
if (timer.current) {
clearInterval(timer.current);
}
closeWsTraffic();
removeSoketEventBus();
clearTimeout(initTimer);
cleanupServices();
console.log("App component unmounted, all services cleaned up");
};
}, []);

View File

@ -32,7 +32,7 @@ export class BlockChainApi {
const res = await api.get(this.baseUrl + "/cosmos/base/node/v1beta1/config");
this.blockConfig = {
halt_height: res.data.halt_height,
minimum_gas_price: res.data.minimum_gas_price || 0.0001,
minimum_gas_price: res.data.minimum_gas_price || 0.00005,
pruning_interval: res.data.pruning_interval,
pruning_keep_recent: res.data.pruning_keep_recent,
}

View File

@ -20,6 +20,17 @@ async getNodes() : Promise<Result<ProxyNodeInfo[], string>> {
else return { status: "error", error: e as any };
}
},
/**
*
*/
async getNodesUpdate() : Promise<Result<ProxyNodeInfo[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_nodes_update") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
*
*/
@ -209,7 +220,7 @@ export type CoreConfig = { socks_port: number; socks_user: string; socks_pass: s
/**
*
*/
export type ProxyNodeInfo = { name: string; ip: string; country_code: string; country_name: string; country_name_zh: string; city_name: string; city_name_zh: string; delay: number; download: number; upload: number; survive_score: number; exit: boolean; use: boolean }
export type ProxyNodeInfo = { name: string; country_code: string; country_name: string; country_name_zh: string; city_name: string; city_name_zh: string; delay: number; download: number; upload: number; survive_score: number; exit: boolean; use: boolean }
/** tauri-specta globals **/

View File

@ -180,7 +180,7 @@ export function useStartupCheck() {
try {
const [versionResult, nodesResult] = await Promise.all([
commands.getServiceVersion(),
commands.getNodes(),
commands.getNodesUpdate(),
]);
const suitableServiceVersion = await app.getVersion();

View File

@ -37,11 +37,11 @@ export default function Layout() {
title: "面向溯源对抗的数据转发",
icon: <PoolSvg className="w-5 h-5" />,
},
// {
// id: 'proxies',
// title: '节点池',
// icon: <PoolSvg className="w-5 h-5" />,
// },
{
id: 'proxies',
title: '节点池',
icon: <PoolSvg className="w-5 h-5" />,
},
];
const handleClickMenu = (index: number) => {

View File

@ -1,4 +0,0 @@
.linkAdd_ComboxContent{
width: 800px !important;
margin-top: 18px;
}

View File

@ -1,152 +0,0 @@
import { FormInstance } from "antd";
import { FormDialog } from "@/components/FormDialog";
import { useDispatch, useSelector } from "react-redux";
import "./index.scss";
import { EllipsisTooltip } from "@/components/Encapsulation";
import NotFailNodeIcon from "@/assets/svg/common/not-fail-node.svg?react";
import NotWarningNodeIcon from "@/assets/svg/common/not-warning-node.svg?react";
import { NODEDIALOGTYPE } from "../../index";
import { cn, getUrl } from "@/lib/utils";
import eventBus, { eventTypes } from "@/utils/eventBus";
import { AppDispatch, RootState } from "@/store";
import { removeMaliciousNodeList, removeNodeDownList } from "@/store/web3Slice";
import { countryCodeMap } from "@/data";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
}
export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = (
props
) => {
const { code, exit = false } = props.proxyInfo;
return (
<div
className={cn(
"w-[251px] flex p-3 rounded-lg group text-[#111322] cursor-pointer",
exit && "hover:bg-[#EFF6FF]",
props.clasName
)}
>
<div className="flex-1 flex items-center justify-end w-full h-7">
<div className="flex-1 flex space-x-3 items-center">
<div className="w-[27px] h-[20px] proxy-item-flag rounded-sm overflow-hidden">
<img
className={cn("w-full h-full object-cover rounded-sm")}
src={getUrl(`image/res/flag3/${code.toLowerCase()}.svg`)}
/>
</div>
<EllipsisTooltip
className="text-lg flex-1 font-semibold"
text={countryCodeMap[code]}
/>
</div>
</div>
</div>
);
};
export const ClearNodeDialog = ({
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit,
}: {
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
}) => {
const dispatch = useDispatch<AppDispatch>();
const { maliciousNodeList, nodeDownList } = useSelector(
(state: RootState) => state.web3Reducer
);
const [isClear, setIsClear] = useState(false);
const showDialog = (open: boolean) => {
setOpen(open);
};
const onSuccessHandle = () => {
successHandle();
setIsClear(true);
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
nodeDownList.forEach((item) => {
dispatch(removeNodeDownList(item.name));
eventBus.emit(eventTypes.NODE_REMOVE, item.name);
});
} else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
maliciousNodeList.forEach((item) => {
dispatch(removeMaliciousNodeList(item.name));
eventBus.emit(eventTypes.NODE_REMOVE, item.name);
});
}
// showDialog(false);
};
const proxyList = useMemo(() => {
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
return nodeDownList;
} else {
return maliciousNodeList;
}
}, [nodeDownList, maliciousNodeList, type.title]);
useEffect(() => {
if (!open) {
setIsClear(false);
}
}, [open]);
return (
<FormDialog
open={open}
openChange={showDialog}
title={type.title}
describe={type.desc}
successText={type.successText}
successHandle={onSuccessHandle}
submitLoading={dialogLoading}
form={form}
contentClass="w-[834px] flex flex-col max-h-[calc(100vh-100px)] overflow-y-hidden "
successStyle={
canSubmit
? "bg-[#dc2626] hover:bg-[#dc2626] active:bg-[#dc2626]"
: "opacity-50"
}
>
<div className="flex flex-wrap gap-3">
{proxyList.length > 0 ? (
proxyList
.filter((item: any) => item?.name)
.map((item: any) => {
return <ProxyItem proxyInfo={item} key={item?.name} />;
})
) : (
<div className="w-full h-[382px] flex flex-col items-center justify-center">
{type.title === NODEDIALOGTYPE.ClearFailNode.title ? (
<NotFailNodeIcon />
) : (
<NotWarningNodeIcon />
)}
<div className="text-lg font-medium text-zinc-950 leading-relaxed mt-5">
{type.title === NODEDIALOGTYPE.ClearFailNode.title
? "暂无掉线节点"
: "暂无恶意节点"}
</div>
</div>
)}
</div>
</FormDialog>
);
};

View File

@ -1,4 +0,0 @@
.linkAdd_ComboxContent{
width: 800px !important;
margin-top: 18px;
}

View File

@ -1,224 +0,0 @@
import { Form, FormInstance } from "antd";
import { Input } from "@/components/ui/input";
import { FormDialog } from "@/components/FormDialog";
import { countryCodeMap } from "@/data";
import { DIALOGTYPE } from "../../index";
import "./index.scss";
import { DefaultLink } from "./pathChoose";
import { Button } from "@/components/ui/button";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
}
const PledgeAmount = ({
value,
onChange,
}: {
value?: string;
onChange?: (data: string) => void;
}) => {
return (
<div className="flex items-center gap-1.5">
<Input
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
placeholder="请输质押金额*"
value={value}
onChange={(e) => {
onChange?.(e.target.value);
}}
/>
<span>SOL</span>
</div>
);
};
const IpPortInput = ({
value,
onChange,
}: {
value?: { port: string; ip: string };
onChange?: (data: { port: string; ip: string }) => void;
}) => {
const [ipAndPort, setIpAndPort] = useState<{
port: string;
ip: string;
}>(value ? value : { port: "", ip: "" });
const handleChagne = ({
key,
val,
}: {
key: "port" | "ip";
val: string;
}) => {
const newValue = value ? { ...value } : { port: "", ip: "" };
newValue[key] = val;
onChange?.(newValue);
};
useEffect(() => {
if (value) {
setIpAndPort(value);
}
}, [value]);
return (
<div className="flex items-center gap-1.5">
<Input
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
placeholder="请输入IP*"
value={ipAndPort?.ip}
onChange={(e) => {
handleChagne?.({ key: "ip", val: e.target.value });
}}
/>
<Input
className="data-[state=checked]:bg-[#1E3A8A] "
placeholder="请输入端口"
type="number"
min={0}
max={65535}
value={ipAndPort?.port}
onChange={(e) => {
handleChagne?.({ key: "port", val: e.target.value });
}}
/>
</div>
);
};
const NodeForm = ({ form }: { form: FormInstance }) => {
return (
<Form
className="-mt-1"
form={form}
name="dynamic_form_nest_item"
autoComplete="off"
layout="vertical"
>
{/* <Form.Item name="uid" className="hidden">
<div className="hidden">uid</div>
</Form.Item> */}
<Form.Item name="nodePublicKey" label="节点身份公钥">
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="节点身份公钥*"
/>
</Form.Item>
<Form.Item name="nodeMetadata" label="节点元数据">
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="请输入TLS Pubkey*"
/>
</Form.Item>
<Form.Item name="ipAndPort" label="IP+端口">
<IpPortInput />
</Form.Item>
<Form.Item name="pledgeAmount" label="质押金额">
<PledgeAmount />
</Form.Item>
<Form.Item name="walletAddress" label="钱包地址">
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="钱包地址*"
/>
</Form.Item>
</Form>
);
};
const NetworkForm = ({ form }: { form: FormInstance }) => {
return (
<Form
className="-mt-1"
form={form}
name="dynamic_form_nest_item"
autoComplete="off"
layout="vertical"
>
<Form.Item
name="inbound"
label="入口节点"
rules={[{ required: true, message: "请选择入口节点" }]}
>
<DefaultLink
des="入口节点"
type="entry"
countries={Object.keys(countryCodeMap)
// .splice(0, 50)
.map((key) => key)}
/>
</Form.Item>
<Form.Item
name="outbound"
label="出口节点"
rules={[{ required: true, message: "请选择出口节点" }]}
>
<DefaultLink
des="出口节点"
type="exit"
countries={Object.keys(countryCodeMap)
// .splice(0, 50)
.map((key) => key)}
/>
</Form.Item>
</Form>
);
};
export const FormAlertDialog = ({
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit,
handleSelectFile,
}: {
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
handleSelectFile: () => void;
}) => {
const showDialog = (open: boolean) => {
setOpen(open);
};
return (
<FormDialog
open={open}
openChange={showDialog}
title={type.title}
describe={type.desc}
successText={type.successText}
successHandle={successHandle}
submitLoading={dialogLoading}
form={form}
contentClass="w-[850px] flex flex-col max-h-[calc(100vh-100px)] overflow-y-hidden "
successStyle={
canSubmit
? "bg-[#1E3A8A] hover:bg-[#1D4ED8] active:bg-[#1E40AF]"
: "bg-[#1E3A8A] hover:bg-[#1E3A8A] opacity-50"
}
>
<Button
className="absolute top-3 right-12 bg-transparent text-zinc-900 border border-zinc-200"
onClick={() => handleSelectFile()}
>
</Button>
{open && type.title === DIALOGTYPE.ADDNode.title ? (
<NodeForm form={form} />
) : (
<NetworkForm form={form} />
)}
</FormDialog>
);
};

View File

@ -1,258 +0,0 @@
import { useMemo, useState, useEffect } from 'react'
// import LinkButtonCol from "@/assets/svg/LinkButtonCol.svg?react";
import Gurad from '@/assets/svg/ProxiesGuard.svg?react'
import Exit from '@/assets/svg/Exit.svg?react'
import Down from '@/assets/svg/down.svg?react'
import HiddenNode from '@/assets/svg/HiddenNode.svg?react'
import InletNodeSvg from '@/assets/svg/InletNode.svg?react'
// import EntryNode from "@/assets/svg/EntryNode.svg?react";
import { cn, getUrl } from '@/lib/utils'
// import { useProxiesExit, useProxiesGuard } from "@/api/link";
// import { Button } from "@/components/ui/button";
import { Combobox, IComboboxValue } from '@/components/Combobox'
// import { ProxiesList } from "@/pages/home/types";
import { SearchOptionText } from '@/pages/home/components/MultipleLinks'
import { countryCodeMap } from '@/pages/home/data'
export const DefaultLink = ({
value,
flag = '',
className,
des,
type = 'hidden',
disabled,
countries,
isChecked = true,
onChange,
}: {
value?: string
des: string
flag?: string
countries: any[]
className?: string
type?: string
disabled?: boolean
isChecked?: boolean
onChange?: (value: IComboboxValue) => void
}) => {
// const ingressName = countryCodeMap[flag.toUpperCase()]
const [ingressName, setIngressName] = useState(des ?? '')
const [ingressOptions, setIngressOptions] = useState<
{
label: string
value: string
icon: JSX.Element
num: number
}[]
>([])
const getOptions = () => {
const options = countries.map((item) => {
const name = countryCodeMap[item.toUpperCase()]
return {
label: name,
value: item,
icon: (
<div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] shrink-0">
<img
src={getUrl(`image/res/flag/${item.toUpperCase()}.png`)}
alt=""
className="w-4 h-4"
/>
</div>
),
num: 0,
}
})
setIngressOptions(options)
}
useEffect(() => {
// type === 'hidden' &&
isChecked && setIngressName(countryCodeMap[flag.toUpperCase()] ?? des)
getOptions()
}, [flag])
useEffect(() => {
if (value) {
setIngressName(value)
}
}, [value])
return (
<div className={cn('flex items-center renderButtonStyle', className)}>
<Combobox
value={ingressName}
typeBorder
onChange={(e) => {
onChange?.(e)
}}
title="输入国家名称"
radioText={SearchOptionText}
downIcon={<Down />}
options={ingressOptions}
placeholder={
type === 'entry'
? '入口节点'
: type === 'hidden'
? '隐匿节点'
: '出口节点'
}
muiltip={false}
contentClass="w-full p-2 pt-0 linkAdd_ComboxContent my-[4px]"
contentProps={{
alignOffset: 0,
}}
disabled={disabled}
comboxTitle={<ComboxTitle type={type} />}
/>
</div>
)
}
export const ComboxTitle = ({ type }: { type: string }) => {
const getNode = useMemo(() => {
switch (type) {
case 'guard':
return (
<>
<Gurad className="mr-2 w-6 h-6" fill="#059669" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
case 'hidden':
return (
<>
<HiddenNode className="mr-2 w-6 h-6" fill="#059669" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
case 'entry':
return (
<>
<InletNodeSvg className="mr-2 w-6 h-6" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
default:
return (
<>
<Exit className="mr-2" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
}
}, [type])
return <div className="flex items-center mb-1 mt-2 p-1">{getNode}</div>
}
// export const PathChoose = ({ value, onChange, pathText, type }:
// {
// value?: string
// type: string
// onChange?: (value: IComboboxValue) => void
// pathText: {
// guard: string
// exit: string
// }
// }
// ) => {
// const [flagText, setFlagText] = useState<ProxiesList>()
// const { data: proxiesList, refetch: refetchGuard } = useProxiesGuard()
// const { data: proxiesExit, refetch: refetchExit } = useProxiesExit()
// const [maxHeightClass, setMaxHeightClass] = useState("max-h-[300px]");
// const currentList = useMemo(() => {
// return type === 'guard' ? proxiesList?.data : proxiesExit?.data
// }, [proxiesList, proxiesExit, type])
// const getOptions = (value_: ProxiesList[]) => {
// const options = value_
// .filter(item => item.name === value)
// .map(item => {
// return {
// label: item.name,
// value: item.name,
// icon: <div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] mr-2 shrink-0">
// {/* <img src={getUrl(`image/res/flag/${item.country_code.toUpperCase()}.png`)} alt="" className="w-4 h-4" /> */}
// </div>,
// num: item.delay,
// }
// })
// return options
// }
// const onHandleChange = (e: IComboboxValue) => {
// const value_ = currentList?.find((index: any) => index.name === e)
// setFlagText(value_)
// onChange?.(e)
// }
// const currentOption = useMemo(() => {
// if (!currentList) return []
// const value_ = currentList.find((index: any) => index.name === value)
// setFlagText(value_)
// return getOptions(currentList)
// }, [currentList])
// useEffect(() => {
// const updateMaxHeight = () => {
// if (window.innerHeight < 1080) {
// setMaxHeightClass("max-h-[260px]");
// } else {
// setMaxHeightClass("max-h-[300px]");
// }
// };
// updateMaxHeight(); // 初始调用一次
// window.addEventListener("resize", updateMaxHeight); // 监听窗口大小变化
// return () => window.removeEventListener("resize", updateMaxHeight); // 清除监听器
// }, []);
// return (
// <>
// <LinkButtonCol className="linkbutton" />
// <div className="flex justify-between items-center mt-3">
// <div className="flex-1 mr-[10px]">
// <div className="mb-2 font-medium">{pathText.guard}</div>
// <DefaultLink
// des={flagText?.ingress_country_name ?? "隐匿节点"}
// flag={flagText?.ingress_country_code}
// type='hidden'
// disabled={!value} />
// </div>
// <div className="flex-1">
// <div className="mb-2 font-medium">{pathText.exit}</div>
// <Combobox
// value={value}
// onChange={onHandleChange}
// typeBorder
// radioText={SearchOptionText}
// downIcon={<Down />}
// options={currentOption}
// onComboxOpen={type === 'guard' ? refetchGuard : refetchExit}
// placeholder='请选择节点'
// muiltip={false}
// contentClass="p-2 pt-0 linkAdd_ComboxContent my-[4px]"
// commandListClassName={maxHeightClass}
// ellipsisTooltipClassName="text-[16px]"
// comboxTitle={<ComboxTitle type={type} />}
// contentProps={{
// 'alignOffset': -380
// }}
// />
// </div>
// </div>
// </>
// )
// }

View File

@ -16,27 +16,7 @@ export interface DialogConfig {
successText: string;
}
const PledgeAmount = ({
value,
onChange,
}: {
value?: string;
onChange?: (data: string) => void;
}) => {
return (
<div className="flex items-center gap-1.5">
<Input
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
placeholder="请输质押金额*"
value={value}
onChange={(e) => {
onChange?.(e.target.value);
}}
/>
<span>SOL</span>
</div>
);
};
const IpPortInput = ({
value,
onChange,

View File

@ -1,6 +1,8 @@
import { Form } from "antd";
import { open as openFile } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
import * as XLSX from "xlsx";
import { every, has } from "lodash-es";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { WorldGeo } from "./components/world-geo";
@ -12,7 +14,7 @@ import AddSvg from "@/assets/svg/home/add.svg?react";
import InterSvg from "@/assets/svg/home/inter.svg?react";
import TrashSvg from "@/assets/svg/home/trash.svg?react";
import { cn } from "@/lib/utils";
import eventBus, { eventTypes } from "@/utils/eventBus";
import {
setProxyInfoProxies,
setProxiesList2,
@ -72,7 +74,8 @@ const DecentralizedElasticNetwork = () => {
await form.validateFields();
const formValue: any = form.getFieldsValue();
if (type.title === DIALOGTYPE.ADDNode.title) {
setOpen(false);
eventBus.emit(eventTypes.NODE_INIT, {});
// setOpen(false);
} else {
const { inbound, outbound } = formValue || {};
if (inbound && outbound) {
@ -100,15 +103,41 @@ const DecentralizedElasticNetwork = () => {
],
});
if (selected && typeof selected === "string") {
if (selected.includes("验证节点包1")) {
console.log("验证节点包1");
// dispatch(setProxiesList1());
const data = await readFile(selected);
// 将二进制数据转换为 ArrayBuffer
const arrayBuffer = data.buffer;
// 使用 XLSX 库解析 Excel 文件
const workbook = XLSX.read(arrayBuffer, { type: "array" });
// 获取第一个工作表
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为 JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet);
// 判断是否为空
if (jsonData.length !== 0) {
console.log("Excel 数据:", jsonData);
// 取第一条数据检查是否包含exit,ip,name,port,private_key,publick_key字段
const firstRow: any = jsonData[0];
// 使用lodash 判断是否包含这些字段
const requiredFields = [
"exit",
"ip",
"name",
"port",
"private_key",
"publick_key",
];
if (every(requiredFields, (field) => has(firstRow, field))) {
jsonData.forEach((item: any) => {
eventBus.emit(eventTypes.NODE_ADD, item);
});
}
setOpen(false);
return;
}
if (selected.includes("验证节点包2")) {
console.log("验证节点包2");
dispatch(setProxiesList2());
}
setOpen(false);
console.error("Excel 文件为空或格式不正确");
}
} catch (err) {
console.error("Error selecting file:", err);
@ -180,7 +209,7 @@ const DecentralizedElasticNetwork = () => {
return (
<div
className="w-[105px] relative carousel-item"
key={`${item.height}-${index}`}
key={`${item.id}-${index}`}
style={{
viewTransitionName: `web3-item-1-${index}`,
}}
@ -204,12 +233,13 @@ const DecentralizedElasticNetwork = () => {
{item.transactions}
</div> */}
<div className="!text-xs">{item?.balanceToFixed} SOL</div>
<div className="!text-xs my-[10px]">
<div className="!text-xs my-[6px]">
{item.txs.length}
</div>
<div className="!text-sm opacity-60">
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">{item.height} H</div>
</div>
</div>
);
@ -246,13 +276,14 @@ const DecentralizedElasticNetwork = () => {
className="w-full h-full"
/>
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-white pl-1.5">
<div className="text-lg">{item.balance} SOL</div>
<div className="!text-xs my-[10px]">
<div className="!text-xs">{item.balance} SOL</div>
<div className="!text-xs my-[6px]">
{item.numberTransactions}
</div>
<div className="!text-sm opacity-60">
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">{item.height} H</div>
</div>
</div>
);

View File

@ -1,4 +0,0 @@
.linkAdd_ComboxContent{
width: 800px !important;
margin-top: 18px;
}

View File

@ -1,152 +0,0 @@
import { FormInstance } from "antd";
import { FormDialog } from "@/components/FormDialog";
import { useDispatch, useSelector } from "react-redux";
import "./index.scss";
import { EllipsisTooltip } from "@/components/Encapsulation";
import NotFailNodeIcon from "@/assets/svg/common/not-fail-node.svg?react";
import NotWarningNodeIcon from "@/assets/svg/common/not-warning-node.svg?react";
import { NODEDIALOGTYPE } from "../../index";
import { cn, getUrl } from "@/lib/utils";
import eventBus, { eventTypes } from "@/utils/eventBus";
import { AppDispatch, RootState } from "@/store";
import { removeMaliciousNodeList, removeNodeDownList } from "@/store/web3Slice";
import { countryCodeMap } from "@/data";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
}
export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = (
props
) => {
const { code, exit = false } = props.proxyInfo;
return (
<div
className={cn(
"w-[251px] flex p-3 rounded-lg group text-[#111322] cursor-pointer",
exit && "hover:bg-[#EFF6FF]",
props.clasName
)}
>
<div className="flex-1 flex items-center justify-end w-full h-7">
<div className="flex-1 flex space-x-3 items-center">
<div className="w-[27px] h-[20px] proxy-item-flag rounded-sm overflow-hidden">
<img
className={cn("w-full h-full object-cover rounded-sm")}
src={getUrl(`image/res/flag3/${code.toLowerCase()}.svg`)}
/>
</div>
<EllipsisTooltip
className="text-lg flex-1 font-semibold"
text={countryCodeMap[code]}
/>
</div>
</div>
</div>
);
};
export const ClearNodeDialog = ({
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit,
}: {
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
}) => {
const dispatch = useDispatch<AppDispatch>();
const { maliciousNodeList, nodeDownList } = useSelector(
(state: RootState) => state.web3Reducer
);
const [isClear, setIsClear] = useState(false);
const showDialog = (open: boolean) => {
setOpen(open);
};
const onSuccessHandle = () => {
successHandle();
setIsClear(true);
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
nodeDownList.forEach((item) => {
dispatch(removeNodeDownList(item.name));
eventBus.emit(eventTypes.NODE_REMOVE, item.name);
});
} else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
maliciousNodeList.forEach((item) => {
dispatch(removeMaliciousNodeList(item.name));
eventBus.emit(eventTypes.NODE_REMOVE, item.name);
});
}
// showDialog(false);
};
const proxyList = useMemo(() => {
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
return nodeDownList;
} else {
return maliciousNodeList;
}
}, [nodeDownList, maliciousNodeList, type.title]);
useEffect(() => {
if (!open) {
setIsClear(false);
}
}, [open]);
return (
<FormDialog
open={open}
openChange={showDialog}
title={type.title}
describe={type.desc}
successText={type.successText}
successHandle={onSuccessHandle}
submitLoading={dialogLoading}
form={form}
contentClass="w-[834px] flex flex-col max-h-[calc(100vh-100px)] overflow-y-hidden "
successStyle={
canSubmit
? "bg-[#dc2626] hover:bg-[#dc2626] active:bg-[#dc2626]"
: "opacity-50"
}
>
<div className="flex flex-wrap gap-3">
{proxyList.length > 0 ? (
proxyList
.filter((item: any) => item?.name)
.map((item: any) => {
return <ProxyItem proxyInfo={item} key={item?.name} />;
})
) : (
<div className="w-full h-[382px] flex flex-col items-center justify-center">
{type.title === NODEDIALOGTYPE.ClearFailNode.title ? (
<NotFailNodeIcon />
) : (
<NotWarningNodeIcon />
)}
<div className="text-lg font-medium text-zinc-950 leading-relaxed mt-5">
{type.title === NODEDIALOGTYPE.ClearFailNode.title
? "暂无掉线节点"
: "暂无恶意节点"}
</div>
</div>
)}
</div>
</FormDialog>
);
};

View File

@ -1,4 +0,0 @@
.linkAdd_ComboxContent{
width: 800px !important;
margin-top: 18px;
}

View File

@ -1,224 +0,0 @@
import { Form, FormInstance } from "antd";
import { Input } from "@/components/ui/input";
import { FormDialog } from "@/components/FormDialog";
import { countryCodeMap } from "@/data";
import { DIALOGTYPE } from "../../index";
import "./index.scss";
import { DefaultLink } from "./pathChoose";
import { Button } from "@/components/ui/button";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
}
const PledgeAmount = ({
value,
onChange,
}: {
value?: string;
onChange?: (data: string) => void;
}) => {
return (
<div className="flex items-center gap-1.5">
<Input
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
placeholder="请输质押金额*"
value={value}
onChange={(e) => {
onChange?.(e.target.value);
}}
/>
<span>SOL</span>
</div>
);
};
const IpPortInput = ({
value,
onChange,
}: {
value?: { port: string; ip: string };
onChange?: (data: { port: string; ip: string }) => void;
}) => {
const [ipAndPort, setIpAndPort] = useState<{
port: string;
ip: string;
}>(value ? value : { port: "", ip: "" });
const handleChagne = ({
key,
val,
}: {
key: "port" | "ip";
val: string;
}) => {
const newValue = value ? { ...value } : { port: "", ip: "" };
newValue[key] = val;
onChange?.(newValue);
};
useEffect(() => {
if (value) {
setIpAndPort(value);
}
}, [value]);
return (
<div className="flex items-center gap-1.5">
<Input
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
placeholder="请输入IP*"
value={ipAndPort?.ip}
onChange={(e) => {
handleChagne?.({ key: "ip", val: e.target.value });
}}
/>
<Input
className="data-[state=checked]:bg-[#1E3A8A] "
placeholder="请输入端口"
type="number"
min={0}
max={65535}
value={ipAndPort?.port}
onChange={(e) => {
handleChagne?.({ key: "port", val: e.target.value });
}}
/>
</div>
);
};
const NodeForm = ({ form }: { form: FormInstance }) => {
return (
<Form
className="-mt-1"
form={form}
name="dynamic_form_nest_item"
autoComplete="off"
layout="vertical"
>
{/* <Form.Item name="uid" className="hidden">
<div className="hidden">uid</div>
</Form.Item> */}
<Form.Item name="nodePublicKey" label="节点身份公钥">
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="节点身份公钥*"
/>
</Form.Item>
<Form.Item name="nodeMetadata" label="节点元数据">
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="请输入TLS Pubkey*"
/>
</Form.Item>
<Form.Item name="ipAndPort" label="IP+端口">
<IpPortInput />
</Form.Item>
<Form.Item name="pledgeAmount" label="质押金额">
<PledgeAmount />
</Form.Item>
<Form.Item name="walletAddress" label="钱包地址">
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="钱包地址*"
/>
</Form.Item>
</Form>
);
};
const NetworkForm = ({ form }: { form: FormInstance }) => {
return (
<Form
className="-mt-1"
form={form}
name="dynamic_form_nest_item"
autoComplete="off"
layout="vertical"
>
<Form.Item
name="inbound"
label="入口节点"
rules={[{ required: true, message: "请选择入口节点" }]}
>
<DefaultLink
des="入口节点"
type="entry"
countries={Object.keys(countryCodeMap)
// .splice(0, 50)
.map((key) => key)}
/>
</Form.Item>
<Form.Item
name="outbound"
label="出口节点"
rules={[{ required: true, message: "请选择出口节点" }]}
>
<DefaultLink
des="出口节点"
type="exit"
countries={Object.keys(countryCodeMap)
// .splice(0, 50)
.map((key) => key)}
/>
</Form.Item>
</Form>
);
};
export const FormAlertDialog = ({
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit,
handleSelectFile,
}: {
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
handleSelectFile: () => void;
}) => {
const showDialog = (open: boolean) => {
setOpen(open);
};
return (
<FormDialog
open={open}
openChange={showDialog}
title={type.title}
describe={type.desc}
successText={type.successText}
successHandle={successHandle}
submitLoading={dialogLoading}
form={form}
contentClass="w-[850px] flex flex-col max-h-[calc(100vh-100px)] overflow-y-hidden "
successStyle={
canSubmit
? "bg-[#1E3A8A] hover:bg-[#1D4ED8] active:bg-[#1E40AF]"
: "bg-[#1E3A8A] hover:bg-[#1E3A8A] opacity-50"
}
>
<Button
className="absolute top-3 right-12 bg-transparent text-zinc-900 border border-zinc-200"
onClick={() => handleSelectFile()}
>
</Button>
{open && type.title === DIALOGTYPE.ADDNode.title ? (
<NodeForm form={form} />
) : (
<NetworkForm form={form} />
)}
</FormDialog>
);
};

View File

@ -1,258 +0,0 @@
import { useMemo, useState, useEffect } from 'react'
// import LinkButtonCol from "@/assets/svg/LinkButtonCol.svg?react";
import Gurad from '@/assets/svg/ProxiesGuard.svg?react'
import Exit from '@/assets/svg/Exit.svg?react'
import Down from '@/assets/svg/down.svg?react'
import HiddenNode from '@/assets/svg/HiddenNode.svg?react'
import InletNodeSvg from '@/assets/svg/InletNode.svg?react'
// import EntryNode from "@/assets/svg/EntryNode.svg?react";
import { cn, getUrl } from '@/lib/utils'
// import { useProxiesExit, useProxiesGuard } from "@/api/link";
// import { Button } from "@/components/ui/button";
import { Combobox, IComboboxValue } from '@/components/Combobox'
// import { ProxiesList } from "@/pages/home/types";
import { SearchOptionText } from '@/pages/home/components/MultipleLinks'
import { countryCodeMap } from '@/pages/home/data'
export const DefaultLink = ({
value,
flag = '',
className,
des,
type = 'hidden',
disabled,
countries,
isChecked = true,
onChange,
}: {
value?: string
des: string
flag?: string
countries: any[]
className?: string
type?: string
disabled?: boolean
isChecked?: boolean
onChange?: (value: IComboboxValue) => void
}) => {
// const ingressName = countryCodeMap[flag.toUpperCase()]
const [ingressName, setIngressName] = useState(des ?? '')
const [ingressOptions, setIngressOptions] = useState<
{
label: string
value: string
icon: JSX.Element
num: number
}[]
>([])
const getOptions = () => {
const options = countries.map((item) => {
const name = countryCodeMap[item.toUpperCase()]
return {
label: name,
value: item,
icon: (
<div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] shrink-0">
<img
src={getUrl(`image/res/flag/${item.toUpperCase()}.png`)}
alt=""
className="w-4 h-4"
/>
</div>
),
num: 0,
}
})
setIngressOptions(options)
}
useEffect(() => {
// type === 'hidden' &&
isChecked && setIngressName(countryCodeMap[flag.toUpperCase()] ?? des)
getOptions()
}, [flag])
useEffect(() => {
if (value) {
setIngressName(value)
}
}, [value])
return (
<div className={cn('flex items-center renderButtonStyle', className)}>
<Combobox
value={ingressName}
typeBorder
onChange={(e) => {
onChange?.(e)
}}
title="输入国家名称"
radioText={SearchOptionText}
downIcon={<Down />}
options={ingressOptions}
placeholder={
type === 'entry'
? '入口节点'
: type === 'hidden'
? '隐匿节点'
: '出口节点'
}
muiltip={false}
contentClass="w-full p-2 pt-0 linkAdd_ComboxContent my-[4px]"
contentProps={{
alignOffset: 0,
}}
disabled={disabled}
comboxTitle={<ComboxTitle type={type} />}
/>
</div>
)
}
export const ComboxTitle = ({ type }: { type: string }) => {
const getNode = useMemo(() => {
switch (type) {
case 'guard':
return (
<>
<Gurad className="mr-2 w-6 h-6" fill="#059669" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
case 'hidden':
return (
<>
<HiddenNode className="mr-2 w-6 h-6" fill="#059669" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
case 'entry':
return (
<>
<InletNodeSvg className="mr-2 w-6 h-6" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
default:
return (
<>
<Exit className="mr-2" />
<span className="text-[#18181B] text-base font-semibold">
</span>
</>
)
}
}, [type])
return <div className="flex items-center mb-1 mt-2 p-1">{getNode}</div>
}
// export const PathChoose = ({ value, onChange, pathText, type }:
// {
// value?: string
// type: string
// onChange?: (value: IComboboxValue) => void
// pathText: {
// guard: string
// exit: string
// }
// }
// ) => {
// const [flagText, setFlagText] = useState<ProxiesList>()
// const { data: proxiesList, refetch: refetchGuard } = useProxiesGuard()
// const { data: proxiesExit, refetch: refetchExit } = useProxiesExit()
// const [maxHeightClass, setMaxHeightClass] = useState("max-h-[300px]");
// const currentList = useMemo(() => {
// return type === 'guard' ? proxiesList?.data : proxiesExit?.data
// }, [proxiesList, proxiesExit, type])
// const getOptions = (value_: ProxiesList[]) => {
// const options = value_
// .filter(item => item.name === value)
// .map(item => {
// return {
// label: item.name,
// value: item.name,
// icon: <div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] mr-2 shrink-0">
// {/* <img src={getUrl(`image/res/flag/${item.country_code.toUpperCase()}.png`)} alt="" className="w-4 h-4" /> */}
// </div>,
// num: item.delay,
// }
// })
// return options
// }
// const onHandleChange = (e: IComboboxValue) => {
// const value_ = currentList?.find((index: any) => index.name === e)
// setFlagText(value_)
// onChange?.(e)
// }
// const currentOption = useMemo(() => {
// if (!currentList) return []
// const value_ = currentList.find((index: any) => index.name === value)
// setFlagText(value_)
// return getOptions(currentList)
// }, [currentList])
// useEffect(() => {
// const updateMaxHeight = () => {
// if (window.innerHeight < 1080) {
// setMaxHeightClass("max-h-[260px]");
// } else {
// setMaxHeightClass("max-h-[300px]");
// }
// };
// updateMaxHeight(); // 初始调用一次
// window.addEventListener("resize", updateMaxHeight); // 监听窗口大小变化
// return () => window.removeEventListener("resize", updateMaxHeight); // 清除监听器
// }, []);
// return (
// <>
// <LinkButtonCol className="linkbutton" />
// <div className="flex justify-between items-center mt-3">
// <div className="flex-1 mr-[10px]">
// <div className="mb-2 font-medium">{pathText.guard}</div>
// <DefaultLink
// des={flagText?.ingress_country_name ?? "隐匿节点"}
// flag={flagText?.ingress_country_code}
// type='hidden'
// disabled={!value} />
// </div>
// <div className="flex-1">
// <div className="mb-2 font-medium">{pathText.exit}</div>
// <Combobox
// value={value}
// onChange={onHandleChange}
// typeBorder
// radioText={SearchOptionText}
// downIcon={<Down />}
// options={currentOption}
// onComboxOpen={type === 'guard' ? refetchGuard : refetchExit}
// placeholder='请选择节点'
// muiltip={false}
// contentClass="p-2 pt-0 linkAdd_ComboxContent my-[4px]"
// commandListClassName={maxHeightClass}
// ellipsisTooltipClassName="text-[16px]"
// comboxTitle={<ComboxTitle type={type} />}
// contentProps={{
// 'alignOffset': -380
// }}
// />
// </div>
// </div>
// </>
// )
// }

View File

@ -22,8 +22,6 @@ import {
} from "@/store/web3Slice";
import type { AppDispatch, RootState } from "@/store";
import "./index.scss";
import { DialogConfig, FormAlertDialog } from "./components/FormAlertDialog";
import { ClearNodeDialog } from "./components/ClearNodeDialog";
import {
getPassAuthentication,
getTrafficObfuscation,
@ -74,10 +72,7 @@ const NewHome = () => {
const [openNode, setOpenNode] = useState(false);
const [dialogLoading] = useState(false);
const [selectedApp, setSelectedApp] = useState<any>(null);
const [type, setType] = useState<DialogConfig>(DIALOGTYPE.ADDNode);
const [nodeType, setNodeType] = useState<DialogConfig>(
NODEDIALOGTYPE.ClearFailNode
);
const [tooltipClosed, setTooltipClosed] = useState(true);
@ -113,60 +108,9 @@ const NewHome = () => {
setSelectedApp(item);
};
async function handleSelectFile() {
try {
const selected = await openFile({
multiple: false,
directory: false,
filters: [
{
name: "Excel Files",
extensions: ["xlsx", "xls"], // 移除了扩展名前的点号
},
],
});
if (selected && typeof selected === "string") {
if (selected.includes("验证节点包1")) {
console.log("验证节点包1");
dispatch(setProxiesList1());
}
if (selected.includes("验证节点包2")) {
console.log("验证节点包2");
dispatch(setProxiesList2());
}
setOpen(false);
}
} catch (err) {
console.error("Error selecting file:", err);
}
}
const ICircuitRequest = useMemo(() => {
return {
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit: true,
handleSelectFile,
};
}, [open, dialogLoading, type, handleSelectFile]);
const nodeSuccessHandle = () => {};
const ClearNodeDialogProps = useMemo(() => {
return {
open: openNode,
setOpen: setOpenNode,
successHandle: nodeSuccessHandle,
dialogLoading,
form,
type: nodeType,
canSubmit: true,
};
}, [openNode, dialogLoading, nodeType]);
const [dataInfo, setDataInfo] = useState<any>({
passAuthentication: {
@ -327,12 +271,15 @@ const NewHome = () => {
{item.transactions}
</div> */}
<div className="!text-xs">{item?.balanceToFixed} SOL</div>
<div className="!text-xs my-[10px]">
<div className="!text-xs my-[6px]">
{item.txs.length}
</div>
<div className="!text-sm opacity-60">
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">
{item.height} H
</div>
</div>
</div>
);
@ -370,12 +317,15 @@ const NewHome = () => {
/>
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-white pl-1.5">
<div className="!text-xs">{item.balance} SOL</div>
<div className="!text-xs my-[10px]">
<div className="!text-xs my-[6px]">
{item.numberTransactions}
</div>
<div className="!text-sm opacity-60">
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">
{item.height} H
</div>
</div>
</div>
);
@ -440,8 +390,7 @@ const NewHome = () => {
);
})}
</div>
<FormAlertDialog {...ICircuitRequest} />
<ClearNodeDialog {...ClearNodeDialogProps} />
</div>
);
};

View File

@ -1,5 +1,5 @@
import { createBrowserRouter, Navigate } from 'react-router-dom'
// import HomePage from '@/pages/home'
import HomePage from '@/pages/home'
import NewHomePage from '@/pages/new-home'
import DecentralizedElasticNetworkPage from '@/pages/decentralized-lastic-network'
import AntiForensicsForwardingPage from '@/pages/anti-forensics-forwarding'
@ -35,7 +35,7 @@ export const router = createBrowserRouter([
// },
{
path: '/proxies',
element: <LazyLoader component={ProxiesPage} />,
element: <LazyLoader component={HomePage} />,
},
],
},

View File

@ -11,7 +11,7 @@ export const eventTypes = {
// 检测到恶意节点
MALICIOUS_NODE: "malicious_node",
// 节点预配置完成
NODE_INIT_COMPLATE: "node_init_complate",
NODE_INIT_COMPLATE: "node_init_complete",
// 节点清除
NODE_REMOVE: "node_remove",
// 添加节点

View File

@ -0,0 +1,245 @@
import WebSocket, {
Message,
ConnectionConfig,
} from '@tauri-apps/plugin-websocket'
export class WebSocketClient {
private ws: WebSocket | null = null
private isConnected = false
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectDelay = 1000 // 初始重连延迟(毫秒)
private listeners: ((msg: Message) => void)[] = []
private autoReconnect = true
// 构造函数初始化 WebSocket 连接
constructor(
private url: string,
private config: ConnectionConfig = {},
options?: {
autoReconnect?: boolean,
maxReconnectAttempts?: number,
reconnectDelay?: number
}
) {
if (options) {
this.autoReconnect = options.autoReconnect ?? true
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5
this.reconnectDelay = options.reconnectDelay ?? 1000
}
}
// 连接到 WebSocket
async connect(): Promise<boolean> {
try {
if (this.isConnected && this.ws) {
console.log('Already connected')
return true
}
this.ws = await WebSocket.connect(this.url, this.config)
this.isConnected = true
this.reconnectAttempts = 0 // 重置重连计数
console.log(`Connected to ${this.url}`)
// 重新添加之前的所有监听器
for (const listener of this.listeners) {
this.ws.addListener(listener)
}
// 添加内部监听器来处理连接关闭
this.ws.addListener(this.handleWebSocketMessage.bind(this))
return true
} catch (error) {
console.error('Failed to connect:', error)
this.isConnected = false
this.ws = null
// 如果启用了自动重连,尝试重连
if (this.autoReconnect) {
return this.attemptReconnect()
}
return false
}
}
// 处理 WebSocket 消息,特别是关闭事件
private async handleWebSocketMessage(msg: Message): Promise<void> {
if (msg.type === 'Close') {
this.isConnected = false
this.ws = null
console.log('Connection closed by server')
// 如果启用了自动重连,尝试重连
if (this.autoReconnect) {
await this.attemptReconnect()
}
}
}
// 尝试重新连接
private async attemptReconnect(): Promise<boolean> {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error(`Failed to reconnect after ${this.maxReconnectAttempts} attempts`)
return false
}
this.reconnectAttempts++
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1) // 指数退避策略
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms...`)
await new Promise(resolve => setTimeout(resolve, delay))
try {
return await this.connect()
} catch (error) {
console.error('Reconnection attempt failed:', error)
return false
}
}
// 手动重连
async reconnect(): Promise<boolean> {
this.reconnectAttempts = 0 // 重置重连计数
if (this.isConnected && this.ws) {
try {
await this.disconnect()
} catch (error) {
console.error('Error during disconnect before reconnect:', error)
}
}
return this.connect()
}
// 发送消息
async sendMessage(message: any): Promise<boolean> {
if (!this.ws || !this.isConnected) {
console.error('WebSocket is not connected')
// 如果启用了自动重连,尝试重连后再发送
if (this.autoReconnect) {
const reconnected = await this.attemptReconnect()
if (!reconnected) return false
} else {
return false
}
}
try {
await this.ws?.send(message)
console.log('Message sent:', message)
return true
} catch (error) {
console.error('Failed to send message:', error)
// 如果发送失败,可能连接已关闭
this.isConnected = false
// 如果启用了自动重连,尝试重连后再发送
if (this.autoReconnect) {
const reconnected = await this.attemptReconnect()
if (reconnected) {
// 重连成功,重试发送消息
return this.sendMessage(message)
}
}
return false
}
}
// 添加消息监听器
addListener(callback: (msg: Message) => void): void {
// 保存监听器以便重连时重新添加
this.listeners.push(callback)
if (!this.ws) {
console.warn('WebSocket is not connected, listener will be added after connection')
return
}
this.ws.addListener(callback)
}
// 断开 WebSocket 连接
async disconnect(): Promise<boolean> {
if (!this.ws) {
console.warn('WebSocket is not connected')
this.isConnected = false
return true
}
try {
await this.ws.disconnect()
this.isConnected = false
this.ws = null
console.log('Disconnected')
return true
} catch (error) {
console.error('Failed to disconnect:', error)
// 即使断开连接失败,也将状态设置为未连接
this.isConnected = false
this.ws = null
return false
}
}
// 检查连接状态
isWebSocketConnected(): boolean {
return this.isConnected && this.ws !== null
}
// 设置自动重连选项
setAutoReconnect(enable: boolean): void {
this.autoReconnect = enable
}
// 设置最大重连尝试次数
setMaxReconnectAttempts(attempts: number): void {
this.maxReconnectAttempts = attempts
}
// 设置重连延迟
setReconnectDelay(delay: number): void {
this.reconnectDelay = delay
}
}
// 使用示例
/*
const webSocketClient = new WebSocketClient(
'ws://127.0.0.1:8080',
{}, // 连接配置
{
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: 1000
}
);
async function init() {
// 连接
const connected = await webSocketClient.connect();
if (connected) {
// 添加消息监听器
webSocketClient.addListener((msg) => {
console.log('Received Message:', msg);
});
// 发送消息
await webSocketClient.sendMessage('Hello World!');
}
}
// 在应用关闭时断开连接
async function cleanup() {
await webSocketClient.disconnect();
}
init();
*/