feat:接口联调试完毕
This commit is contained in:
parent
5000c88f0f
commit
fae2e8e7ec
6
.env
6
.env
@ -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"
|
||||
@ -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
82
pnpm-lock.yaml
generated
@ -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
1
src-tauri/Cargo.lock
generated
@ -2960,6 +2960,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-os",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm web:dev",
|
||||
"devUrl": "http://127.0.0.1:1420",
|
||||
"beforeBuildCommand": "pnpm web:build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
|
||||
433
src/App.tsx
433
src/App.tsx
@ -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");
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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 **/
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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",
|
||||
// 添加节点
|
||||
|
||||
245
src/utils/webSocketClientV2.ts
Normal file
245
src/utils/webSocketClientV2.ts
Normal 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();
|
||||
*/
|
||||
Loading…
x
Reference in New Issue
Block a user