Compare commits

...

37 Commits

Author SHA1 Message Date
liyuanhu
22d7d4cf3c config:env 2025-05-12 16:05:45 +08:00
liyuanhu
8efc3f9b31 feat:给日志追加当前时间 2025-05-12 15:28:00 +08:00
liyuanhu
176aa823b1 config:env 2025-05-12 14:09:46 +08:00
liyuanhu
4b2e53ac22 feat:优化日志,以及嵌套加密和动态路由不能同时存在连线 2025-05-12 14:08:43 +08:00
liyuanhu
46a53d7b46 feat:新增每个节点弹窗demo 2025-05-12 11:50:36 +08:00
liyuanhu
a785561aa9 chore:删除默认配置 2025-05-08 11:28:46 +08:00
liyuanhu
e420fb0c1e chore:修改timeout 的时间 2025-05-08 10:28:38 +08:00
liyuanhu
a0e931c472 fix:终极优化 2025-05-07 18:14:05 +08:00
liyuanhu
30a1a40a8c feat:websoket连接更换 2025-05-07 17:15:41 +08:00
liyuanhu
cbe3cb594e chore:更新资源 2025-05-07 16:46:23 +08:00
liyuanhu
afbb74f823 fix:修改了api 地址 2025-05-07 14:43:04 +08:00
liyuanhu
f5d23a68c3 feat:新增核心 2025-04-28 10:30:28 +08:00
liyuanhu
bdad7bc1da feat:数据对接完毕 2025-04-28 10:26:57 +08:00
liyuanhu
21d8ddabf1 chore:首页兼容了屏幕大小变化 流量混淆嵌套加密等tips弹窗 跟随变化 2025-04-27 11:38:01 +08:00
liyuanhu
91a7ca8c4b style:样式修复 2025-04-27 11:24:11 +08:00
liyuanhu
5151bd35d0 feat:mock了一下后端数据 以及logs日志的接受传输格式,完善首页流量混淆和嵌套加密交互 2025-04-27 11:16:17 +08:00
liyuanhu
15bb6d8bc0 chore:git .gitignore 2025-04-25 18:40:08 +08:00
liyuanhu
6357dfb24c feat:新增一些日志功能 2025-04-25 18:39:15 +08:00
liyuanhu
a9d30c05c8 fix:慢点连线 2025-04-25 15:14:39 +08:00
liyuanhu
bab441979f feat:131的一些修改 2025-04-24 19:02:35 +08:00
liyuanhu
1928b112fb feat:131的一些修改 2025-04-24 18:53:32 +08:00
liyuanhu
32f38e1efb chore:加强了文件格式的校验和错误提示 2025-04-23 15:29:33 +08:00
liyuanhu
2efe64862d fix:bug 2025-04-23 15:02:22 +08:00
liyuanhu
e80e4035a0 fix:改了一下文案 2025-04-23 10:14:37 +08:00
liyuanhu
c753617ee9 feat: 新增--traffic-obfuscate 2025-04-23 10:03:27 +08:00
liyuanhu
a86b20f9d5 chore:删除打包文件 2025-04-22 18:27:57 +08:00
liyuanhu
98a4e9b388 fix:bug 2025-04-22 17:39:24 +08:00
liyuanhu
bd6f658c73 fix:打包配置 2025-04-22 16:27:08 +08:00
liyuanhu
fae2e8e7ec feat:接口联调试完毕 2025-04-22 16:04:37 +08:00
liyuanhu
5000c88f0f fix:bug 2025-04-21 19:32:32 +08:00
liyuanhu
1693316766 feat: 新增websoket事件对应的events 2025-04-21 14:40:24 +08:00
liyuanhu
d220b7b0f5 feat: 新增131nodes 接口数据转 proxies 格式 2025-04-18 19:21:52 +08:00
liyuanhu
ffabc1ccbe fix:bug 2025-04-18 11:12:35 +08:00
liyuanhu
9f09e8ba61 refactor:重构请求配置 2025-04-17 14:12:49 +08:00
liyuanhu
064973aaf0 chore:删除不必要的代码 2025-04-16 19:18:21 +08:00
liyuanhu
41dba8cb7c fix:嵌套加密和流量混淆 改为接口返回的点 2025-04-16 19:17:30 +08:00
liyuanhu
652aea1829 feat:首页接口对接 2025-04-16 19:00:51 +08:00
70 changed files with 9594 additions and 4931 deletions

14
.env
View File

@ -1,5 +1,11 @@
GRPC_ENDPOINT="156.229.167.121:9090"
COSMOS_ENDPOINT="http://156.229.167.121:26657"
NODE_SECRET="aHVnZSBjb21wYW55IHBob25lIHdlc3QgcGxhY2Ugc2VtaW5hciBtaXJhY2xlIGxlbmQgbWFuZGF0ZSB0aGVuIGFkanVzdCBxdWl0IG1lYXQgY2hlYXAgbm9vZGxlIGNvdXBsZSBkZWZpbmUgbXVzY2xlIHB1bHNlIHNpc3RlciBwaWVjZSBkZXZpY2UgcHJpdmF0ZSBob29k"
IS_DEBUG="true"
ACCOUNT_NAME="de1"
GRPC_ENDPOINT="47.82.97.10:9090"
COSMOS_ENDPOINT="http://47.82.97.10:26657"
ACCOUNT_NAME="bchrmll0jvrr"
TRAFFIC_OBFUSCATE="47.82.97.10:8180"
NODE_SECRET= "shell useless annual satisfy comfort bulk beyond own fly puzzle key shock grief depart capable purpose peasant model stamp city gain parent bright auto"
VIET_EVENTS_URL="ws://47.82.97.10:8080/events"
VITE_BASE_URL="http://47.82.97.10:8080"
VITE_BLOCK_URL="http://47.82.97.10:1317"
NODE_OPTIONS=--max-old-space-size=8192
# $env:NODE_OPTIONS="--max-old-space-size=8192"; pnpm run web:build

12
.env copy Normal file
View File

@ -0,0 +1,12 @@
IS_DEBUG="true"
GRPC_ENDPOINT="10.66.66.230:9090"
COSMOS_ENDPOINT="http://10.66.66.230:26657"
ACCOUNT_NAME="bltrj0xs"
TRAFFIC_OBFUSCATE="10.66.66.230:8180"
NODE_SECRET= "protect curtain print mixed void mammal husband bamboo dinner butter beach off leader frost medal pig deputy orphan indoor wolf buyer emerge pause dog"
VIET_EVENTS_URL="ws://10.66.66.230:8080/events"
VITE_BASE_URL="http://10.66.66.230:8080"
VITE_BLOCK_URL="http://10.66.66.230:1317"
NODE_OPTIONS=--max-old-space-size=8192

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",
@ -48,6 +49,7 @@
"i18next": "^24.2.0",
"lodash-es": "^4.17.21",
"lucide-react": "^0.469.0",
"mitt": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.54.2",
@ -60,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"
},

109
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
@ -119,6 +122,9 @@ importers:
lucide-react:
specifier: ^0.469.0
version: 0.469.0(react@18.3.1)
mitt:
specifier: ^3.0.1
version: 3.0.1
react:
specifier: ^18.2.0
version: 18.3.1
@ -155,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
@ -569,42 +578,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.0':
resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.0':
resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.0':
resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.0':
resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
@ -1320,55 +1323,46 @@ packages:
resolution: {integrity: sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.27.4':
resolution: {integrity: sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.27.4':
resolution: {integrity: sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.27.4':
resolution: {integrity: sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.27.4':
resolution: {integrity: sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.27.4':
resolution: {integrity: sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.27.4':
resolution: {integrity: sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.27.4':
resolution: {integrity: sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.27.4':
resolution: {integrity: sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.27.4':
resolution: {integrity: sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==}
@ -1490,28 +1484,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.1.0':
resolution: {integrity: sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.1.0':
resolution: {integrity: sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.1.0':
resolution: {integrity: sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.1.0':
resolution: {integrity: sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==}
@ -1539,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==}
@ -1622,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'}
@ -1717,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'}
@ -1751,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'}
@ -1787,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'}
@ -1899,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==}
@ -2097,6 +2111,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
@ -2688,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==}
@ -2898,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'}
@ -2906,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==}
@ -4164,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
@ -4270,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
@ -4411,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
@ -4471,6 +4516,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
codepage@1.15.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -4500,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
@ -4618,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:
@ -4782,6 +4833,8 @@ snapshots:
minipass@7.1.2: {}
mitt@3.0.1: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
@ -5435,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:
@ -5655,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
@ -5667,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: {}

View File

@ -8,6 +8,5 @@
.vscode/*
sidecar/*
system-service/.vscode/*

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

@ -23,6 +23,7 @@ pub struct CoreConfig {
pub node_secret: String,
pub passphrase: String,
pub data_path: String,
pub traffic_obfuscate: String,
}
impl Default for CoreConfig {
@ -54,6 +55,7 @@ impl Default for CoreConfig {
// 2. 在 `paw-gui/src-tauri/common` 文件夹下的 `build.rs` 中增加条件检查
// 在发布的情况下,应当从编译期环境变量中获取真实配置,而非默认配置
let traffic_obfuscate = env!("TRAFFIC_OBFUSCATE").to_string();
let account_name = env!("ACCOUNT_NAME").to_string();
let grpc_endpoint = env!("GRPC_ENDPOINT").to_string();
let cosmos_endpoint = env!("COSMOS_ENDPOINT").to_string();
@ -80,6 +82,7 @@ impl Default for CoreConfig {
node_secret,
passphrase: "".to_string(),
data_path,
traffic_obfuscate,
}
}
}
@ -105,6 +108,7 @@ impl CoreConfig {
format!("-nodeSecret={}", self.node_secret),
format!("-passphrase={}", self.passphrase),
format!("-data-path={}", self.data_path),
format!("-traffic-obfuscate={}", self.traffic_obfuscate),
];
args

Binary file not shown.

Binary file not shown.

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

@ -59,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(1500)) // 由于正常情况下节点获取速度很快而且是本地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
@ -76,7 +98,7 @@ pub async fn get_nodes(config: CoreConfig) -> Result<Vec<ProxyNodeInfo>> {
debug!("Successfully got nodes: {:?}", result.data);
Ok(result.data)
} else {
error!("Failed to get nodes: {}", response.text().await?);
debug!("Failed to get nodes: {}", response.text().await?);
bail!("Failed to 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

@ -1,15 +1,26 @@
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 { commands } from '@/bindings'
import { useCoreConfig } from "@/hooks/useCoreConfig";
import { useDispatch } from "react-redux";
import {
setMaliciousNodeList,
setNodeDownList,
setWeb3List,
setWeb3List2,
} from "@/store/web3Slice";
import type { AppDispatch } from "@/store";
import eventBus, { eventTypes } from "@/utils/eventBus";
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";
function App() {
// 执行启动自检
useStartupCheck();
@ -23,6 +34,290 @@ function App() {
const { loadCoreConfig } = useCoreConfig();
loadCoreConfig();
const dispatch = useDispatch<AppDispatch>();
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.error("Error processing WebSocket message:", error);
}
};
// 初始化 WebSocket 连接
const initWebSocket = async () => {
if (eventsWsRef.current) return;
console.log("Initializing WebSocket connection...");
eventsWsRef.current = new WebSocketClient(
"ws://47.82.97.10: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 startBlockchainPolling = () => {
// 立即执行一次
fetchBlockchainData();
// 设置定时器定期获取数据
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(() => {
// 延迟初始化以确保组件完全挂载
const initTimer = setTimeout(() => {
initializeServices();
}, 1000);
// 组件卸载时清理资源
return () => {
clearTimeout(initTimer);
cleanupServices();
console.log("App component unmounted, all services cleaned up");
};
}, []);
return (
<main>

View File

@ -1,13 +1,26 @@
import { CoreConfig } from "@/bindings";
import { api } from "@/utils/api";
type BlockConfig = {
halt_height: string;
minimum_gas_price: string;
pruning_interval: string;
pruning_keep_recent: string;
}
// 区块链api 类
export class BlockChainApi {
public coreConfig: CoreConfig;
public baseUrl = "http://localhost:3000/api";
public blockConfig: BlockConfig
public baseUrl = import.meta.env.VITE_BLOCK_URL;
// public baseUrl = "http://47.82.97.10:26657";
constructor() {
this.coreConfig = {} as CoreConfig;
this.blockConfig = {} as BlockConfig;
this.getBlockConfig();
}
// async initCoreConfig() {
@ -15,13 +28,41 @@ export class BlockChainApi {
// this.coreConfig = await loadCoreConfig();
// }
async getBlockConfig(){
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.00005,
pruning_interval: res.data.pruning_interval,
pruning_keep_recent: res.data.pruning_keep_recent,
}
return res;
}
// 获取最新的区块信息
async getLatestBlock() {
const res = await api.get(
this.baseUrl + "/cosmos/base/tendermint/v1beta1/blocks/latest",
this.baseUrl + "/cosmos/base/tendermint/v1beta1/blocks/latest"
);
return res
return res;
}
// cosmos/tx/v1beta1/txs/block
async getTxsByBlock(blockHeight: number) {
const res = await api.get(
this.baseUrl + `/cosmos/tx/v1beta1/txs/block/${blockHeight}`
);
return res;
}
async getNodeInfo() {
const res = await api.get(
this.baseUrl + "/cosmos/base/tendermint/v1beta1/node_info"
);
return res;
}
}
// 导出一个异步函数 initBlockChinApi用于初始化区块链API

27
src/api/flying-line.ts Normal file
View File

@ -0,0 +1,27 @@
import { api } from "@/utils/api";
const baseUrl = import.meta.env.VITE_BASE_URL;
// 通信认证
export const getPassAuthentication = async () => {
return (await api.get(`${baseUrl}/step/pass_authentication`)).data;
};
// 流量混淆
export const getTrafficObfuscation = async () => {
return (await api.get(`${baseUrl}/step/traffic_obfuscation`)).data;
};
// 嵌套加密
export const getNestedEncryption = async () => {
return (await api.get(`${baseUrl}/step/nested_encryption`)).data;
};
// 动态路由生成
export const getDynamicRouteGeneration = async () => {
return (await api.get(`${baseUrl}/step/dynamic_route_generator`)).data;
};
// 应用分流
export const getApplicationDiversion = async () => {
return (await api.get(`${baseUrl}/step/app_diversion`)).data;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

View File

@ -0,0 +1,5 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon / &#230;&#137;&#147;&#229;&#135;&#187;&#232;&#138;&#130;&#231;&#130;&#185;">
<path id="Icon " fill-rule="evenodd" clip-rule="evenodd" d="M11.1652 1.11022C11.5045 1.25753 11.7061 1.61092 11.6602 1.978L10.944 7.70797L16.1065 7.70797C16.3073 7.70793 16.5107 7.70789 16.6769 7.72294C16.8341 7.73717 17.135 7.77482 17.3961 7.98574C17.6949 8.22721 17.8661 8.59267 17.8603 8.97682C17.8552 9.31237 17.6914 9.56768 17.6018 9.69753C17.5069 9.83483 17.3767 9.99106 17.2481 10.1453L9.80685 19.0748C9.57002 19.359 9.17419 19.453 8.83484 19.3057C8.49549 19.1584 8.29388 18.805 8.33977 18.4379L9.05602 12.708L3.89353 12.708C3.6927 12.708 3.48929 12.7081 3.3231 12.693C3.16593 12.6788 2.86495 12.6411 2.60394 12.4302C2.30513 12.1887 2.13395 11.8233 2.13975 11.4391C2.1448 11.1036 2.30856 10.8483 2.39825 10.7184C2.49309 10.5811 2.62334 10.4249 2.75194 10.2706C2.75831 10.263 2.76468 10.2553 2.77103 10.2477L10.1932 1.34115C10.43 1.05696 10.8258 0.962915 11.1652 1.11022ZM4.2792 11.0413H10C10.239 11.0413 10.4665 11.1439 10.6247 11.3231C10.7829 11.5023 10.8565 11.7408 10.8269 11.978L10.3461 15.8243L15.7208 9.37464H10C9.76098 9.37464 9.53346 9.272 9.37527 9.09281C9.21708 8.91362 9.14346 8.67513 9.1731 8.43794L9.65388 4.59169L4.2792 11.0413Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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 };
}
},
/**
*
*/
@ -205,7 +216,7 @@ async getDynamicRoutingEndpoint() : Promise<Result<string, string>> {
/** user-defined types **/
export type CircuitRequest = { name: string | null; inbound: string; outbound: string; multi_hop: number; fallback: boolean; rule_path: string | null; is_prefix: boolean }
export type CoreConfig = { socks_port: number; socks_user: string; socks_pass: string; api_port: number; secret: string; tun: boolean; log_file_path: string; traffic_gen_rule_path: string; dsn: string; debug: boolean; address_prefix: string; account_name: string; grpc_endpoint: string; cosmos_endpoint: string; node_secret: string; passphrase: string; data_path: string }
export type CoreConfig = { socks_port: number; socks_user: string; socks_pass: string; api_port: number; secret: string; tun: boolean; log_file_path: string; traffic_gen_rule_path: string; dsn: string; debug: boolean; address_prefix: string; account_name: string; grpc_endpoint: string; cosmos_endpoint: string; node_secret: string; passphrase: string; data_path: string; traffic_obfuscate: string }
/**
*
*/

View File

@ -56,22 +56,29 @@ export function ServiceControlPanel() {
}
// 处理代理开关
// 定义一个异步函数 handleProxyToggle用于处理代理开关的切换
const handleProxyToggle = async (checked: boolean) => {
// 如果开关被选中且电路未准备好,则输出错误信息并返回
if (checked && !isCircuitReady) {
console.error('请先创建电路')
return
}
// 设置代理加载状态为 true表示正在处理代理开关的切换
setIsProxyLoading(true)
try {
// 如果开关被选中,则调用 enableProxy 函数启用代理
if (checked) {
await dispatch(enableProxy()).unwrap()
// await dispatch(enableProxy()).unwrap()
} else {
// 如果开关未被选中,则调用 disableProxy 函数禁用代理
await dispatch(disableProxy()).unwrap()
}
} catch (error) {
// 如果在启用或禁用代理时发生错误,则输出错误信息
console.error('Proxy toggle failed:', error)
} finally {
// 无论启用或禁用代理是否成功,最后都将代理加载状态设置为 false
setIsProxyLoading(false)
}
}

File diff suppressed because it is too large Load Diff

26
src/hooks/useEventBus.ts Normal file
View File

@ -0,0 +1,26 @@
// src/hooks/useEventBus.ts
import { useEffect } from 'react';
import eventBus from '@/utils/eventBus';
export function useEventBus(
event:any,
handler:any,
) {
useEffect(() => {
// 添加事件监听器
eventBus.on(event, handler as any);
// 清理函数
return () => {
eventBus.off(event, handler as any);
};
}, [event, handler]); // 依赖项包含 event 和 handler
// 返回发布事件的函数
return {
emit: (eventName:any, data: any) => {
eventBus.emit(eventName, data);
}
};
}

View File

@ -1,20 +1,21 @@
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { v4 as uuidv4 } from 'uuid'
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
// import { v4 as uuidv4 } from "uuid";
import { commands, ProxyNodeInfo } from '@/bindings'
import { app } from '@tauri-apps/api'
import { createCircuit } from '@/store/circuitSlice'
import { setServiceStatus, getProxy, getVersion } from '@/store/serviceSlice'
import { setNodes, setNodesError, clearNodes } from '@/store/nodesSlice'
import { commands, ProxyNodeInfo } from "@/bindings";
import { app } from "@tauri-apps/api";
// import { createCircuit } from "@/store/circuitSlice";
import { setServiceStatus, getProxy, getVersion } from "@/store/serviceSlice";
import { setNodes, setNodesError, clearNodes } from "@/store/nodesSlice";
import { setProxiesList1 } from "@/store/web3Slice";
// import { clearCircuitState } from '@/store/circuitSlice';
import { AppDispatch, RootState } from '@/store'
import { AppDispatch, RootState } from "@/store";
interface StartupCheckResult {
success: boolean
error?: string
data?: ProxyNodeInfo[]
success: boolean;
error?: string;
data?: ProxyNodeInfo[];
}
// 重试函数:最多重试指定次数,每次等待指定时间
@ -22,27 +23,27 @@ async function retry<T>(
operation: () => Promise<T>,
maxAttempts: number,
delayMs: number,
validate: (result: T) => boolean,
validate: (result: T) => boolean
): Promise<T> {
let lastError: Error | undefined
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const result = await operation()
const result = await operation();
if (validate(result)) {
return result
return result;
}
lastError = new Error(`Validation failed on attempt ${attempt}`)
lastError = new Error(`Validation failed on attempt ${attempt}`);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
lastError = error instanceof Error ? error : new Error(String(error));
}
if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delayMs))
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
throw lastError || new Error('Operation failed after all attempts')
throw lastError || new Error("Operation failed after all attempts");
}
// 正常来说windows下和linux下使用包管理器更新时安装/卸载服务是足够的只有macOS下依赖启动检查来安装/卸载服务
@ -50,44 +51,44 @@ async function retry<T>(
async function checkAndInstallService(): Promise<StartupCheckResult> {
try {
// 获取当前服务版本和期望版本
const versionResult = await commands.getServiceVersion()
const expectedVersion = await app.getVersion()
const versionResult = await commands.getServiceVersion();
const expectedVersion = await app.getVersion();
// 需要安装/重装的两种情况,分别是
// 1. 服务不存在
// 2. 服务存在但版本不匹配
const serviceNotExists = versionResult.status === 'error'
const serviceNotExists = versionResult.status === "error";
const versionMismatch =
!serviceNotExists && versionResult.data !== expectedVersion
!serviceNotExists && versionResult.data !== expectedVersion;
if (serviceNotExists) {
console.log('Service not found, installing...')
const installResult = await commands.installService()
if (installResult.status === 'error') {
console.log("Service not found, installing...");
const installResult = await commands.installService();
if (installResult.status === "error") {
return {
success: false,
error: 'Failed to install service',
}
error: "Failed to install service",
};
}
} else if (versionMismatch) {
console.log('Service version mismatch, uninstalling old service...')
const uninstallResult = await commands.uninstallService()
if (uninstallResult.status === 'error') {
console.log("Service version mismatch, uninstalling old service...");
const uninstallResult = await commands.uninstallService();
if (uninstallResult.status === "error") {
return {
success: false,
error: 'Failed to uninstall old service',
}
error: "Failed to uninstall old service",
};
}
await new Promise((resolve) => setTimeout(resolve, 5000))
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log('Installing new service...')
const installResult = await commands.installService()
if (installResult.status === 'error') {
console.log("Installing new service...");
const installResult = await commands.installService();
if (installResult.status === "error") {
return {
success: false,
error: 'Failed to install service',
}
error: "Failed to install service",
};
}
}
@ -100,41 +101,41 @@ async function checkAndInstallService(): Promise<StartupCheckResult> {
() => commands.getServiceVersion(),
10,
1000,
(result) => result.status === 'ok' && result.data === expectedVersion,
)
(result) => result.status === "ok" && result.data === expectedVersion
);
} catch (error) {
return {
success: false,
error: 'Service installation verification failed after retries',
}
error: "Service installation verification failed after retries",
};
}
}
return { success: true }
return { success: true };
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error during service check',
}
: "Unknown error during service check",
};
}
}
async function checkAndStartCore(): Promise<StartupCheckResult> {
try {
// 检查核心状态
let coreStatus = await commands.getNodes()
let coreStatus = await commands.getNodes();
// 如果核心未运行,尝试启动
if (coreStatus.status === 'error') {
console.log('Starting core...')
const startResult = await commands.restartCore()
if (startResult.status === 'error') {
if (coreStatus.status === "error") {
console.log("Starting core...");
const startResult = await commands.restartCore();
if (startResult.status === "error") {
return {
success: false,
error: 'Failed to start core',
}
error: "Failed to start core",
};
}
// 等待并验证核心是否成功启动
@ -144,166 +145,176 @@ async function checkAndStartCore(): Promise<StartupCheckResult> {
() => commands.getNodes(),
10,
500,
(result) => result.status === 'ok',
)
(result) => result.status === "ok"
);
} catch (error) {
return {
success: false,
error: 'Core start verification failed after retries',
}
error: "Core start verification failed after retries",
};
}
}
return {
success: true,
data: coreStatus.status === 'ok' ? coreStatus.data : undefined,
}
data: coreStatus.status === "ok" ? coreStatus.data : undefined,
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error during core check',
}
: "Unknown error during core check",
};
}
}
export function useStartupCheck() {
const dispatch: AppDispatch = useDispatch()
const dispatch: AppDispatch = useDispatch();
const { fallbackCircuit } = useSelector(
(state: RootState) => state.circuitReducer,
)
(state: RootState) => state.circuitReducer
);
// 检查服务和核心状态,同时更新节点信息
const checkStatus = async () => {
try {
const [versionResult, nodesResult] = await Promise.all([
commands.getServiceVersion(),
commands.getNodes(),
])
commands.getNodesUpdate(),
]);
const suitableServiceVersion = await app.getVersion()
const suitableServiceVersion = await app.getVersion();
const isServiceInstalled =
versionResult.status === 'ok' &&
versionResult.data === suitableServiceVersion
const isCoreRunning = nodesResult.status === 'ok'
versionResult.status === "ok" &&
versionResult.data === suitableServiceVersion;
const isCoreRunning = nodesResult.status === "ok";
// 更新服务状态
dispatch(
setServiceStatus({
isServiceInstalled,
isCoreRunning,
}),
)
})
);
// 更新节点信息
// 只有当核心正在运行时才更新节点
if (isCoreRunning) {
if (nodesResult.status === 'ok') {
dispatch(setNodes(nodesResult.data))
if (nodesResult.status === "ok") {
const proxies:any[] = [];
nodesResult.data.forEach((node,index) => {
const { country_code } = node
if(nodesResult.data.length-1 === index){
return
}
proxies.push({ country_code: country_code,ingress_country_code: nodesResult.data[index + 1].country_code })
})
// console.log(proxies,'proxiesproxiesproxies')
dispatch(setProxiesList1(proxies))
dispatch(setNodes(nodesResult.data));
} else {
dispatch(setNodesError('Failed to fetch nodes'))
dispatch(setNodesError("Failed to fetch nodes"));
}
} else {
dispatch(clearNodes())
dispatch(clearNodes());
// dispatch(clearCircuitState()); // 当核心未运行时清空链路状态
}
} catch (error) {
console.error('Status check failed:', error)
console.error("Status check failed:", error);
dispatch(
setServiceStatus({
isServiceInstalled: false,
isCoreRunning: false,
}),
)
dispatch(clearNodes())
})
);
dispatch(clearNodes());
}
}
};
useEffect(() => {
async function performStartupCheck() {
try {
// 步骤1检查并安装服务
const serviceResult = await checkAndInstallService()
const serviceResult = await checkAndInstallService();
if (!serviceResult.success) {
console.error('Service check failed:', serviceResult.error)
console.error("Service check failed:", serviceResult.error);
await dispatch(
setServiceStatus({
isServiceInstalled: false,
isCoreRunning: false,
}),
)
await dispatch(clearNodes())
return
})
);
await dispatch(clearNodes());
return;
}
// 步骤2检查并启动核心
const coreResult = await checkAndStartCore()
const coreResult = await checkAndStartCore();
if (!coreResult.success) {
console.error('Core check failed:', coreResult.error)
console.error("Core check failed:", coreResult.error);
await dispatch(
setServiceStatus({
isServiceInstalled: true,
isCoreRunning: false,
}),
)
await dispatch(clearNodes())
return
})
);
await dispatch(clearNodes());
return;
}
// 步骤3检查当前核心版本
await dispatch(getVersion())
await dispatch(getVersion());
// 步骤4检查当前是否开启了代理
await dispatch(getProxy())
await dispatch(getProxy());
// 步骤5检查当前是否有默认链路并且保证状态是正常否则重新创建一个默认链路
if (!fallbackCircuit || fallbackCircuit?.state === 'failed') {
let inbound = ''
let outbound = ''
if (coreResult && coreResult.data && coreResult.data.length > 0) {
inbound = coreResult.data[0].country_code || ''
outbound =
coreResult.data
.filter((item) => item.exit && item.country_code !== inbound)
?.slice(-1)?.[0].country_code || ''
// 如果没有这两个那么就跳过,证明核心运行了但是节点是空的,如果只有其中一个,那么证明总共节点就一个无法创建链路
if (inbound && outbound) {
await dispatch(
createCircuit({
uid: uuidv4(),
name: '系统默认链路',
inbound: inbound,
outbound: outbound,
multi_hop: 3,
fallback: true,
rule_path: null,
is_prefix: false,
}),
).unwrap()
}
}
}
// // 步骤5检查当前是否有默认链路并且保证状态是正常否则重新创建一个默认链路
// if (!fallbackCircuit || fallbackCircuit?.state === "failed") {
// let inbound = "";
// let outbound = "";
// if (coreResult && coreResult.data && coreResult.data.length > 0) {
// inbound = coreResult.data[0].country_code || "";
// outbound =
// coreResult.data
// .filter((item) => item.exit && item.country_code !== inbound)
// ?.slice(-1)?.[0].country_code || "";
// // 如果没有这两个那么就跳过,证明核心运行了但是节点是空的,如果只有其中一个,那么证明总共节点就一个无法创建链路
// if (inbound && outbound) {
// await dispatch(
// createCircuit({
// uid: uuidv4(),
// name: "系统默认链路",
// inbound: inbound,
// outbound: outbound,
// multi_hop: 3,
// fallback: true,
// rule_path: null,
// is_prefix: false,
// })
// ).unwrap();
// }
// }
// }
//
// 执行一次完整的状态检查
await checkStatus()
await checkStatus();
} catch (error) {
console.error('Startup check failed:', error)
console.error("Startup check failed:", error);
dispatch(
setServiceStatus({
isServiceInstalled: false,
isCoreRunning: false,
}),
)
dispatch(clearNodes())
})
);
dispatch(clearNodes());
}
}
// 执行启动自检
performStartupCheck()
performStartupCheck();
// 每5秒检查一次状态
const intervalId = setInterval(checkStatus, 5000)
return () => clearInterval(intervalId)
}, [])
const intervalId = setInterval(checkStatus, 5000);
return () => clearInterval(intervalId);
}, []);
}

View File

@ -7,6 +7,20 @@ body {
}
.scrollbar-visible {
scrollbar-width: auto;
/* For Firefox */
-ms-overflow-style: scrollbar;
/* For Internet Explorer and Edge */
}
.scrollbar-visible::-webkit-scrollbar {
display: block;
/* For Chrome, Safari, and Opera */
height: 10px;
}
::selection {
// background-color: #18181b;
background-color: #1E3A8A;
@ -17,7 +31,7 @@ body {
::-webkit-scrollbar {
width: 6px;
height: 6px;
/* background-color: red; */
// background-color: red;
}
/* ::-webkit-scrollbar-track {
@ -104,3 +118,186 @@ body {
user-select: none;
/* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
}
// // 添加到 index.scss
.decentralized {
background-color: #0f172a;
// background-image: linear-gradient(180deg, #172554 0%, #0A0F2A 100%);
.box {
width: 100%;
height: 100%;
background-image: url("@/assets/image/line-bg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
position: absolute;
left: 0;
top: 0;
mix-blend-mode: lighten;
}
.web3-line::after {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #7D82FF;
position: absolute;
left: 50%;
top: 0px;
z-index: 999;
transform: translate(-50%, 0);
}
}
// // 轮播容器样式
.carousel-container {
display: flex;
align-items: center;
gap: 3rem; // 对应原来的gap-12
// width: 100%;
}
.bt1 {
display: flex;
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
justify-content: center;
align-items: center;
gap: var(--8-spacing-04, 8px);
border-radius: var(--radius-6, 6px);
border: 1px solid var(--Colors-Bluepurple-600, #4136F5);
background: var(--button-wireframe-button-wireframe, rgba(9, 9, 11, 0.00));
box-shadow: 0px 0px 4px 0px var(--Colors-Bluepurple-500, #5457FF), 0px 0px 10px 0px var(--Colors-Bluepurple-600, #4136F5);
color: var(--text-text-primary-900, #FFF);
/* Text/Medium/T5文本1 */
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
// line-height: 24px;
/* 171.429% */
}
.bt2 {
display: flex;
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
justify-content: center;
align-items: center;
gap: var(--8-spacing-04, 8px);
border-radius: var(--radius-6, 6px);
border: 1px solid var(--Colors-Rose-600, #E11D48);
background: var(--button-wireframe-button-wireframe, rgba(255, 255, 255, 0.00));
box-shadow: 0px 0px 4px 0px var(--Colors-Rose-600, #E11D48), 0px 0px 10px 0px var(--Colors-Rose-600, #E11D48);
color: var(--text-text-primary-900, #FFF);
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
// line-height: 24px;
}
.tip-box-left {
position: relative;
width: 600px;
height: 400px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
outline: 0.46px solid white;
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.label {
width: 100%;
color: #FFF;
font-size: 18px;
font-weight: 600;
line-height: 16px;
}
.close-icon,
.close-icon2 {
width: 16px;
height: 16px;
position: absolute;
top: 20px;
right: 20px;
color: #FFF;
}
}
.tip-box-hx {
position: relative;
width: 600px;
height: 400px;
margin-left: 312.221px;
// min-height: 200px;
// max-height: 600px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
outline: 0.46px solid white;
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.close-icon,
.close-icon2 {
width: 16px;
height: 16px;
position: absolute;
top: 20px;
right: 20px;
color: #FFF;
}
.label {
width: 100%;
color: #FFF;
font-size: 18px;
font-weight: 600;
line-height: 16px;
}
}
.encryption-img {
width: 526px;
height: 241px;
margin-left: 16px;
}
.traffic-obfuscation-img {
width: 597px;
height: 241px;
margin-left: 16px;
}
.tooltip-content {
position: relative;
display: flex;
.line-img-hx {
width: 312.221px;
// margin-top: 80px;
top: 80px;
left: 0px;
position: absolute;
}
.line-img-left {
width: 216.86px;
margin-top: -72px;
}
}

View File

@ -9,94 +9,102 @@ import TitleSvg from "@/assets/svg/layout/title.svg?react";
import ChevronDownSvg from "@/assets/svg/layout/chevron-down.svg?react";
import Decentralized from "@/assets/svg/layout/decentralized.svg?react";
import PoolSvg from '@/assets/svg/layout/pool.svg?react'
import HomeSvg from '@/assets/svg/layout/home.svg?react'
import PoolSvg from "@/assets/svg/layout/pool.svg?react";
import HomeSvg from "@/assets/svg/layout/home.svg?react";
import AntiDarkAnalysisNetworkSvg from "@/assets/svg/layout/anti-dark-analysis-network.svg?react";
import "./index.scss";
import type { RootState } from "@/store";
export default function Layout() {
const [_, setActive] = useState(0);
const { coreVersion } = useSelector(
(state: RootState) => state.serviceReducer
);
const [_, setActive] = useState(0);
const { coreVersion } = useSelector(
(state: RootState) => state.serviceReducer
);
const navList = [
{
id: "new-home",
title: "首页",
icon: <HomeSvg className="w-5 h-5" />,
},
{
id: "home",
title: "去中心化的弹性网络",
icon: <Decentralized className="w-5 h-5" />,
},
{
id: "anti-forensics-forwarding",
title: "面向溯源对抗的数据转发",
icon: <PoolSvg className="w-5 h-5" />,
},
// {
// id: 'proxies',
// title: '节点池',
// icon: <PoolSvg className="w-5 h-5" />,
// },
];
const navList = [
{
id: "new-home",
title: "首页",
icon: <HomeSvg className="w-5 h-5" />,
},
{
id: "home",
title: "去中心化的弹性网络",
icon: <Decentralized className="w-5 h-5" />,
},
{
id: "anti-forensics-forwarding",
title: "面向溯源对抗的数据转发",
icon: <PoolSvg className="w-5 h-5" />,
},
{
id: "anti-dark-analysis-network",
title: "抗暗特征分析的隐匿网络应用",
icon: <AntiDarkAnalysisNetworkSvg className="w-5 h-5" />,
},
// {
// id: 'proxies',
// title: '节点池',
// icon: <PoolSvg className="w-5 h-5" />,
// },
];
const handleClickMenu = (index: number) => {
setActive(index);
};
const handleClickMenu = (index: number) => {
setActive(index);
};
return (
<div data-tauri-drag-region className="layout flex">
<div
data-tauri-drag-region
className={cn(
"px-2 py-3 select-none side w-[240px] h-full overflow-hidden flex flex-col gap-y-3 flex-shrink-0 relative",
isMac && "mt-6"
)}
>
<header className="flex items-center">
<LogoSvg className="w-8 h-8" />
<div className="ml-[9px] flex flex-col items-center justify-center">
<TitleSvg />
<AnonymousSvg />
{/* <span className="text-white text-[18px] font-bold tracking-wide">匿名反溯源网络系统</span> */}
{/* <span className="text-white text-[8px] font-medium">Anonymous anti traceability network system</span> */}
</div>
</header>
<nav className="flex flex-col flex-1 gap-y-2">
{navList.map((item, index) => {
return (
<NavLink
key={item.id}
to={"/" + item.id}
className={({ isActive }) =>
cn(
"px-[11px] py-2 flex items-center gap-2 rounded text-white text-sm",
isActive && "bg-[#213265] "
)
}
onClick={() => handleClickMenu(index)}
>
{item.icon}
<span>{item.title}</span>
</NavLink>
);
})}
</nav>
<footer className="h-[55px] p-4 items-center gap-2 inline-flex absolute left-4 bottom-[20px]">
<div className="w-[156px] text-white/40 text-sm font-normal leading-tight">
<div>:{coreVersion || "v0.0.1"}</div>
{/* <div>环境DEV</div> */}
</div>
<ChevronDownSvg fill="rgba(255, 255, 255, 0.4)" />
</footer>
</div>
<main className="flex-1 bg-white mt-10 mb-1 mr-1 rounded-xl overflow-hidden min-w-[1693px]">
<Outlet />
</main>
return (
<div data-tauri-drag-region className="layout flex">
<div
data-tauri-drag-region
className={cn(
"px-2 py-3 select-none side w-[240px] h-full overflow-hidden flex flex-col gap-y-3 flex-shrink-0 relative",
isMac && "mt-6"
)}
>
<header className="flex items-center">
<LogoSvg className="w-8 h-8" />
<div className="ml-[9px] flex flex-col items-center justify-center">
<TitleSvg />
<AnonymousSvg />
{/* <span className="text-white text-[18px] font-bold tracking-wide">匿名反溯源网络系统</span> */}
{/* <span className="text-white text-[8px] font-medium">Anonymous anti traceability network system</span> */}
</div>
</header>
<nav className="flex flex-col flex-1 gap-y-2">
{navList.map((item, index) => {
return (
<NavLink
key={item.id}
to={"/" + item.id}
className={({ isActive }) =>
cn(
"pl-[11px] py-2 flex items-center gap-2 rounded text-white text-sm",
isActive && "bg-[#213265] "
)
}
onClick={() => handleClickMenu(index)}
>
{item.icon}
<span>{item.title}</span>
</NavLink>
);
})}
</nav>
<footer className="h-[55px] p-4 items-center gap-2 inline-flex absolute left-4 bottom-[20px]">
<div className="w-[156px] text-white/40 text-sm font-normal leading-tight">
<div>:{coreVersion || "v0.0.1"}</div>
{/* <div>环境DEV</div> */}
</div>
<ChevronDownSvg fill="rgba(255, 255, 255, 0.4)" />
</footer>
</div>
<main className="flex-1 bg-white mt-10 mb-1 mr-1 rounded-xl overflow-y-hidden overflow-x-auto w-full scrollbar-visible">
<div className="min-w-[1693px] h-full">
<Outlet />
</div>
);
</main>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,449 @@
export const TRAFFIC_OBFUSCATION = {
type: "NESTED_ENCRYPTION",
name: "流量混淆",
code: "ZA",
data: [
{
country_code: "gl",
ingress_country_code: "za",
},
{
country_code: "za",
ingress_country_code: "dz",
},
{
country_code: "dz",
ingress_country_code: "ru",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
],
isLine: true,
};
export const NESTED_ENCRYPTION = {
type: "NESTED_ENCRYPTION",
name: "嵌套加密",
code: "GL",
data: [
{
country_code: "gl",
ingress_country_code: "br",
},
{
country_code: "br",
ingress_country_code: "dz",
},
{
country_code: "dz",
ingress_country_code: "ru",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
],
isLine: true,
};
export const DYNAMIC_ROUTE_GENERATOR = [
{
type: "DYNAMIC_ROUTE_GENERATOR",
name: "动态路由生成",
data: [
{
country_code: "us",
ingress_country_code: "ca",
},
{
country_code: "ca",
ingress_country_code: "gl",
},
{
country_code: "gl",
ingress_country_code: "by",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
],
color: "#48D3D5",
isLine: true,
},
{
type: "DYNAMIC_ROUTE_GENERATOR",
name: "动态路由生成2",
data: [
{
country_code: "br",
ingress_country_code: "ml",
},
{
country_code: "ml",
ingress_country_code: "ly",
},
{
country_code: "ly",
ingress_country_code: "cn",
},
{
country_code: "cn",
ingress_country_code: "ru",
},
],
color: "#50FE35",
isLine: true,
},
];
export const APP_DIVERSION = [
{
name: "Netflix",
color: "#DC2626",
data: [
{
country_code: "mg",
ingress_country_code: "ru",
},
{
country_code: "ru",
ingress_country_code: "fr",
},
{
country_code: "fr",
ingress_country_code: "br",
},
{
country_code: "br",
ingress_country_code: "us",
},
],
isLine: true,
},
{
name: "Spotify",
color: "#22C55E",
data: [
{
country_code: "jp",
ingress_country_code: "au",
},
{
country_code: "au",
ingress_country_code: "za",
},
{
country_code: "za",
ingress_country_code: "de",
},
{
country_code: "de",
ingress_country_code: "ca",
},
],
isLine: true,
},
{
name: "Instagram",
color: "#8B5CF6",
data: [
{
country_code: "it",
ingress_country_code: "in",
},
{
country_code: "in",
ingress_country_code: "mx",
},
{
country_code: "mx",
ingress_country_code: "se",
},
{
country_code: "se",
ingress_country_code: "sg",
},
],
isLine: true,
},
{
name: "Telegram",
color: "#2563EB",
data: [
{
country_code: "ar",
ingress_country_code: "nl",
},
{
country_code: "nl",
ingress_country_code: "kr",
},
{
country_code: "kr",
ingress_country_code: "eg",
},
{
country_code: "eg",
ingress_country_code: "nz",
},
],
isLine: true,
},
{
name: "Google",
color: "#3B82F6",
data: [
{
country_code: "ch",
ingress_country_code: "br",
},
{
country_code: "br",
ingress_country_code: "hk",
},
{
country_code: "hk",
ingress_country_code: "no",
},
{
country_code: "no",
ingress_country_code: "ae",
},
],
isLine: true,
},
{
name: "Gmail",
color: "#22C55E",
data: [
{
country_code: "es",
ingress_country_code: "cn",
},
{
country_code: "cn",
ingress_country_code: "co",
},
{
country_code: "co",
ingress_country_code: "fi",
},
{
country_code: "fi",
ingress_country_code: "id",
},
],
isLine: true,
},
{
name: "Amazon",
color: "#EAB308",
data: [
{
country_code: "gb",
ingress_country_code: "th",
},
{
country_code: "th",
ingress_country_code: "cl",
},
{
country_code: "cl",
ingress_country_code: "be",
},
{
country_code: "be",
ingress_country_code: "ph",
},
],
isLine: true,
},
{
name: "Ebay",
color: "#3B82F6",
data: [
{
country_code: "pl",
ingress_country_code: "my",
},
{
country_code: "my",
ingress_country_code: "pe",
},
{
country_code: "pe",
ingress_country_code: "dk",
},
{
country_code: "dk",
ingress_country_code: "ng",
},
],
isLine: true,
},
{
name: "AppleNews",
color: "#EF4444",
data: [
{
country_code: "ie",
ingress_country_code: "vn",
},
{
country_code: "vn",
ingress_country_code: "ma",
},
{
country_code: "ma",
ingress_country_code: "at",
},
{
country_code: "at",
ingress_country_code: "tw",
},
],
isLine: true,
},
{
name: "CNN",
color: "#EF4444",
data: [
{
country_code: "ua",
ingress_country_code: "sa",
},
{
country_code: "sa",
ingress_country_code: "gr",
},
{
country_code: "gr",
ingress_country_code: "pk",
},
{
country_code: "pk",
ingress_country_code: "pt",
},
],
isLine: true,
},
{
name: "Browser",
color: "#8B5CF6",
data: [
{
country_code: "il",
ingress_country_code: "ro",
},
{
country_code: "ro",
ingress_country_code: "nz",
},
{
country_code: "nz",
ingress_country_code: "tr",
},
{
country_code: "tr",
ingress_country_code: "ca",
},
],
isLine: true,
},
{
name: "YouTube",
color: "#EF4444",
data: [
{
country_code: "cz",
ingress_country_code: "sg",
},
{
country_code: "sg",
ingress_country_code: "ar",
},
{
country_code: "ar",
ingress_country_code: "hu",
},
{
country_code: "hu",
ingress_country_code: "jp",
},
],
isLine: true,
},
{
name: "Facebook",
color: "#3B82F6",
data: [
{
country_code: "is",
ingress_country_code: "za",
},
{
country_code: "za",
ingress_country_code: "mx",
},
{
country_code: "mx",
ingress_country_code: "it",
},
{
country_code: "it",
ingress_country_code: "kr",
},
],
isLine: true,
},
];
export const PASS_AUTHENTICATION = {
type: "PASS_AUTHENTICATION",
name: "通信认证",
startPoint: "GL",
endPoint: "CA",
authenticationPoint: [
[-103.346771, 54.130366],
[-120.346771, 52.130366],
[-108.346771, 48.130366],
[-98.346771, 46.130366],
[-106.346771, 48.450366],
[-101.346771, 53.130366],
[-123.346771, 58.130366],
[-111.346771, 65.443366],
[-108.346771, 54.130366],
[-116.346771, 59.130366],
[-97.346771, 61.130366],
[-95.346771, 63.130366],
[-113.346771, 58.840366],
[-99.346771, 59.130366],
[-102.346771, 68.130366],
],
data: [
{
country_code: "gl",
ingress_country_code: "dz",
},
{
country_code: "br",
ingress_country_code: "dz",
},
{
country_code: "dz",
ingress_country_code: "ru",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
{
country_code: "ru",
ingress_country_code: "za",
},
],
isLine: true,
}

View File

@ -0,0 +1,310 @@
import { useState } from "react";
import { useSelector } from "react-redux";
import { WorldGeo } from "./components/world-geo";
import NetflixSvg from "@/assets/svg/anti-forensics-forwarding/Netflix.svg?react";
import NetflixActiveSvg from "@/assets/svg/anti-forensics-forwarding/NetflixActive.svg?react";
import SpotifySvg from "@/assets/svg/anti-forensics-forwarding/Spotify.svg?react";
import SpotifyActiveSvg from "@/assets/svg/anti-forensics-forwarding/SpotifyActive.svg?react";
import InstagramSvg from "@/assets/svg/anti-forensics-forwarding/Instagram.svg?react";
import InstagramActiveSvg from "@/assets/svg/anti-forensics-forwarding/InstagramActive.svg?react";
import TelegramSvg from "@/assets/svg/anti-forensics-forwarding/Telegram.svg?react";
import TelegramActiveSvg from "@/assets/svg/anti-forensics-forwarding/TelegramActive.svg?react";
import GoogleSvg from "@/assets/svg/anti-forensics-forwarding/Google.svg?react";
import GoogleActiveSvg from "@/assets/svg/anti-forensics-forwarding/GoogleActive.svg?react";
import GmailSvg from "@/assets/svg/anti-forensics-forwarding/Gmail.svg?react";
import GmailActiveSvg from "@/assets/svg/anti-forensics-forwarding/GmailActive.svg?react";
import AmazonSvg from "@/assets/svg/anti-forensics-forwarding/Amazon.svg?react";
import AmazonActiveSvg from "@/assets/svg/anti-forensics-forwarding/AmazonActive.svg?react";
import EbaySvg from "@/assets/svg/anti-forensics-forwarding/Ebay.svg?react";
import EbayActiveSvg from "@/assets/svg/anti-forensics-forwarding/EbayActive.svg?react";
import AppleNewsSvg from "@/assets/svg/anti-forensics-forwarding/AppleNews.svg?react";
import AppleNewsActiveSvg from "@/assets/svg/anti-forensics-forwarding/AppleNewsActive.svg?react";
import CNNSvg from "@/assets/svg/anti-forensics-forwarding/CNN.svg?react";
import CNNActiveSvg from "@/assets/svg/anti-forensics-forwarding/CNNActive.svg?react";
import BrowserSvg from "@/assets/svg/anti-forensics-forwarding/Browser.svg?react";
import BrowserActiveSvg from "@/assets/svg/anti-forensics-forwarding/BrowserActive.svg?react";
import YouTubeSvg from "@/assets/svg/anti-forensics-forwarding/YouTube.svg?react";
import YouTubeActiveSvg from "@/assets/svg/anti-forensics-forwarding/YouTubeActive.svg?react";
import FacebookSvg from "@/assets/svg/anti-forensics-forwarding/Facebook.svg?react";
import FacebookActiveSvg from "@/assets/svg/anti-forensics-forwarding/FacebookActive.svg?react";
import { RootState } from "@/store";
import "./index.scss";
import {
getApplicationDiversion,
getDynamicRouteGeneration,
getNestedEncryption,
getPassAuthentication,
getTrafficObfuscation,
} from "@/api/flying-line";
export const DIALOGTYPE = {
ADDNode: {
title: "添加节点",
desc: "",
successText: "添加",
},
AddNetwork: {
title: "构建网络",
desc: "",
successText: "构建",
},
};
export const NODEDIALOGTYPE = {
ClearFailNode: {
title: "清除掉线节点",
desc: "",
successText: "清除",
},
ClearWargingNode: {
title: "恶意节点",
desc: "",
successText: "清除",
},
};
export const Apps = [
{
name: "Netflix",
icon: NetflixSvg,
activeIcon: NetflixActiveSvg,
},
{
name: "Spotify",
icon: SpotifySvg,
activeIcon: SpotifyActiveSvg,
},
{
name: "Instagram",
icon: InstagramSvg,
activeIcon: InstagramActiveSvg,
},
{
name: "Telegram",
icon: TelegramSvg,
activeIcon: TelegramActiveSvg,
},
{
name: "Google",
icon: GoogleSvg,
activeIcon: GoogleActiveSvg,
},
{
name: "Gmail",
icon: GmailSvg,
activeIcon: GmailActiveSvg,
},
{
name: "Amazon",
icon: AmazonSvg,
activeIcon: AmazonActiveSvg,
},
{
name: "Ebay",
icon: EbaySvg,
activeIcon: EbayActiveSvg,
},
{
name: "AppleNews",
icon: AppleNewsSvg,
activeIcon: AppleNewsActiveSvg,
},
{
name: "CNN",
icon: CNNSvg,
activeIcon: CNNActiveSvg,
},
{
name: "Browser",
icon: BrowserSvg,
activeIcon: BrowserActiveSvg,
},
{
name: "YouTube",
icon: YouTubeSvg,
activeIcon: YouTubeActiveSvg,
},
{
name: "Facebook",
icon: FacebookSvg,
activeIcon: FacebookActiveSvg,
},
];
export const CONST_TOOLTIP_TYPE = {
NESTED_ENCRYPTION: {
type: "NESTED_ENCRYPTION",
title: "嵌套加密",
},
TRAFFIC_OBFUSCATION: {
type: "TRAFFIC_OBFUSCATION",
title: "流量混淆",
},
DYNAMIC_ROUTE_GENERATOR: {
type: "DYNAMIC_ROUTE_GENERATOR",
title: "动态路由生成",
},
// 应用分流
APP_DIVERSION: {
type: "APP_DIVERSION",
title: "应用分流",
},
// 通信认证
PASS_AUTHENTICATION: {
type: "PASS_AUTHENTICATION",
title: "通信认证",
},
};
const AntiDarkAnalysisNetwork = () => {
const { newHomeProxies } = useSelector(
(state: RootState) => state.web3Reducer
);
const [tooltipType, setTooltipType] = useState(
CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type
);
const [tooltipClosed, setTooltipClosed] = useState(false);
const [selectedApp, setSelectedApp] = useState<any>(null);
const [dataInfo, setDataInfo] = useState<any>(null);
const [trafficObfuscationLogs, setTrafficObfuscationLogs] = useState<any>([
]);
const currentValue = useMemo(() => {
let value = dataInfo;
switch (tooltipType) {
case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
value = selectedApp ? [selectedApp] : [];
break;
default:
break;
}
return value;
}, [tooltipType, selectedApp, dataInfo]);
const handleClickApp = (item: any) => {
setSelectedApp(item);
};
const getDataInfo = async () => {
let value = [];
switch (tooltipType) {
case CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type:
const nestedEncryption = await getNestedEncryption();
value = [nestedEncryption.data];
break;
case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type:
const trafficObfuscation = await getTrafficObfuscation();
value = [trafficObfuscation.data];
setTrafficObfuscationLogs(trafficObfuscation.logs);
break;
case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type:
const dynamicRouteGeneration = await getDynamicRouteGeneration();
value = dynamicRouteGeneration.data;
break;
// case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
// const applicationDiversion = await getApplicationDiversion();
// value = selectedApp ? [selectedApp] : [];
// break;
case CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type:
const passAuthentication = await getPassAuthentication();
value = [passAuthentication.data];
break;
default:
break;
}
setDataInfo(value);
};
const [appData, setAppData] = useState<any>([]);
const appDiversion = useMemo(() => {
return Apps.map((item) => {
const findApp = appData.find(
(appItem: any) => item.name === appItem.name
);
return {
...item,
...findApp,
};
});
}, [appData]);
const initData = async () => {
const applicationDiversion = await getApplicationDiversion();
setAppData(applicationDiversion.data);
setTooltipType(CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type);
setTooltipClosed(true);
};
useEffect(() => {
getDataInfo();
}, [tooltipType]);
useEffect(() => {
initData();
() => {
setTooltipClosed(false);
};
}, []);
return (
<div className="decentralized w-full h-full flex flex-col relative">
<div className="flex items-center gap-[60px] absolute top-12 left-12 z-10">
{tooltipType === CONST_TOOLTIP_TYPE.APP_DIVERSION.type &&
appDiversion.map((item) => {
return (
<div
key={item.name}
className="flex items-center justify-center w-16 h-16 relative rounded-[4.95px] shadow-[0px_0px_3.299999952316284px_0px_rgba(84,87,255,1.00)] outline outline-[0.50px] outline-offset-[-0.50px] outline-indigo-50/60 overflow-hidden"
onClick={() => handleClickApp(item)}
>
{selectedApp?.name === item?.name ? (
<item.activeIcon />
) : (
<item.icon />
)}
</div>
);
})}
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo
trafficObfuscationLogs={trafficObfuscationLogs}
currentValue={currentValue}
newHomeProxies={newHomeProxies}
tooltipType={tooltipType}
tooltipClosed={tooltipClosed}
setTooltipClosed={setTooltipClosed}
/>
</div>
<div className="absolute bottom-6 left-[50%] translate-x-[-50%] w-[calc(100%-51px)] p-6 bg-indigo-950 bg-opacity-10 rounded-md outline outline-1 outline-zinc-200 outline-opacity-40 backdrop-blur-lg inline-flex justify-start items-center gap-4">
<div
className="bt1 cursor-pointer"
onClick={() => {
setTooltipType(CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type);
setTooltipClosed(true);
}}
>
</div>
<div
className="bt1 cursor-pointer"
onClick={() => {
setTooltipType(CONST_TOOLTIP_TYPE.APP_DIVERSION.type);
}}
>
</div>
</div>
</div>
);
};
export default AntiDarkAnalysisNetwork;

View File

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

View File

@ -1,180 +0,0 @@
import { FormInstance } from "antd";
import { FormDialog } from "@/components/FormDialog";
import { useDispatch, useSelector } from "react-redux";
import { nodeList,getRandomNodes } from "@/store/datas";
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 {
setClearFailTimer,
setClearWarningTimer,
} from "@/store/web3Slice";
import { AppDispatch, RootState } from "@/store";
import { isTimestampPlusTenMinutesBeforeNow } from "@/utils/tools";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
}
export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = (
props
) => {
const { name, 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={name}
/>
</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 { clearWarningTimer, clearFailTimer } = 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) {
dispatch(setClearFailTimer(Date.now()));
} else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
dispatch(setClearWarningTimer(Date.now()));
}
// showDialog(false);
};
const proxyList = useMemo(() => {
const newData = getRandomNodes(nodeList);
if (open) {
if (isClear) return [];
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
if (clearFailTimer) {
const clear =
isTimestampPlusTenMinutesBeforeNow(clearFailTimer);
if (clear) {
return newData
} else {
return [];
}
} else {
return newData;
}
}
if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
if (clearWarningTimer) {
const clear =
isTimestampPlusTenMinutesBeforeNow(clearWarningTimer);
console.log(clear,'clear')
if (clear) {
return newData
} else {
return [];
}
} else {
return newData;
}
}
return newData;
}
// 随机 2-9条 nodelist里面的数据
return [];
}, [nodeList, open, isClear, clearFailTimer, clearWarningTimer, type]);
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.map((item) => {
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

@ -499,7 +499,6 @@ export const WorldGeo = memo(
label: {
show: false,
formatter: (params: any) => {
console.log(params, "params");
return `{${params.data.datas.country_code}|}`;
},
},

File diff suppressed because it is too large Load Diff

View File

@ -473,7 +473,6 @@ export const WorldGeo = memo(
label: {
show: false,
formatter: (params: any) => {
console.log(params, "params");
return `{${params.data.datas.country_code}|}`;
},
},

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ export const screenData = {
account: "admin",
account_is_admin: true,
exclusive: "none",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
proxies: ["意大利-米兰-312", "南苏丹-朱巴-374"],
proxies_code: ["it", "ss"],
use: true,
@ -14,7 +14,7 @@ export const screenData = {
proxy_info: {
exclusive: "",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
wg: false,
change_time: 0,
change_at: 0,

View File

@ -2,7 +2,7 @@
export const TRAFFIC_OBFUSCATION = {
type: "NESTED_ENCRYPTION",
name: "流量混淆",
code: "BR",
code: "ZA",
data: [
{
country_code: "gl",
@ -21,7 +21,7 @@ export const TRAFFIC_OBFUSCATION = {
ingress_country_code: "cn",
},
],
isLine: true,
isLine: false,
};
export const NESTED_ENCRYPTION = {
type: "NESTED_ENCRYPTION",
@ -45,34 +45,34 @@ export const NESTED_ENCRYPTION = {
ingress_country_code: "cn",
},
],
isLine: true,
isLine: false,
};
export const DYNAMIC_ROUTE_GENERATOR = [
{
type: "DYNAMIC_ROUTE_GENERATOR",
name: "动态路由生成",
data: [
{
country_code: "us",
ingress_country_code: "ca",
},
{
country_code: "ca",
ingress_country_code: "gl",
},
{
country_code: "gl",
ingress_country_code: "by",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
],
color: "#48D3D5",
isLine: true,
},
// {
// type: "DYNAMIC_ROUTE_GENERATOR",
// name: "动态路由生成",
// data: [
// {
// country_code: "us",
// ingress_country_code: "ca",
// },
// {
// country_code: "ca",
// ingress_country_code: "gl",
// },
// {
// country_code: "gl",
// ingress_country_code: "by",
// },
// {
// country_code: "dz",
// ingress_country_code: "cn",
// },
// ],
// color: "#48D3D5",
// isLine: true,
// },
{
type: "DYNAMIC_ROUTE_GENERATOR",
name: "动态路由生成2",
@ -95,7 +95,7 @@ export const DYNAMIC_ROUTE_GENERATOR = [
},
],
color: "#50FE35",
isLine: true,
isLine: false,
},
];
@ -401,9 +401,11 @@ export const APP_DIVERSION = [
},
];
export const PASS_AUTHENTICATION = {
export const PASS_AUTHENTICATION = {
type: "PASS_AUTHENTICATION",
name: "通行认证",
name: "通信认证",
startPoint: "GL",
endPoint: "CA",
authenticationPoint: [
[-103.346771, 54.130366],
[-120.346771, 52.130366],
@ -422,26 +424,7 @@ export const PASS_AUTHENTICATION = {
[-102.346771, 68.130366],
],
data: [
{
country_code: "gl",
ingress_country_code: "dz",
},
{
country_code: "br",
ingress_country_code: "dz",
},
{
country_code: "dz",
ingress_country_code: "ru",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
{
country_code: "ru",
ingress_country_code: "za",
},
],
isLine: true,
};
}

View File

@ -1,211 +0,0 @@
// // 添加到 index.scss
.decentralized {
background-color: #0f172a;
// background-image: linear-gradient(180deg, #172554 0%, #0A0F2A 100%);
.box {
width: 100%;
height: 100%;
background-image: url("@/assets/image/line-bg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
position: absolute;
left: 0;
top: 0;
mix-blend-mode: lighten;
}
.web3-line::after {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #7D82FF;
position: absolute;
left: 50%;
top: 0px;
z-index: 999;
transform: translate(-50%, 0);
}
}
// // 轮播容器样式
.carousel-container {
display: flex;
align-items: center;
gap: 3rem; // 对应原来的gap-12
// width: 100%;
}
.bt1 {
display: flex;
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
justify-content: center;
align-items: center;
gap: var(--8-spacing-04, 8px);
border-radius: var(--radius-6, 6px);
border: 1px solid var(--Colors-Bluepurple-600, #4136F5);
background: var(--button-wireframe-button-wireframe, rgba(9, 9, 11, 0.00));
box-shadow: 0px 0px 4px 0px var(--Colors-Bluepurple-500, #5457FF), 0px 0px 10px 0px var(--Colors-Bluepurple-600, #4136F5);
color: var(--text-text-primary-900, #FFF);
/* Text/Medium/T5文本1 */
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
// line-height: 24px;
/* 171.429% */
}
.bt2 {
display: flex;
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
justify-content: center;
align-items: center;
gap: var(--8-spacing-04, 8px);
border-radius: var(--radius-6, 6px);
border: 1px solid var(--Colors-Rose-600, #E11D48);
background: var(--button-wireframe-button-wireframe, rgba(255, 255, 255, 0.00));
box-shadow: 0px 0px 4px 0px var(--Colors-Rose-600, #E11D48), 0px 0px 10px 0px var(--Colors-Rose-600, #E11D48);
color: var(--text-text-primary-900, #FFF);
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
// line-height: 24px;
}
.tip-box {
position: relative;
width: 626px;
height: 281px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
outline: 0.46px solid white;
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.close-icon {
width: 16px;
height: 16px;
position: absolute;
top: 20px;
right: 20px;
color: #FFF;
}
.label {
width: 100%;
color: #FFF;
font-size: 18px;
font-weight: 600;
line-height: 16px;
}
.encryption-img {
width: 526px;
height: 241px;
margin-left: 16px;
}
.traffic-obfuscation-img{
width: 597px;
height: 241px;
margin-left: 16px;
}
}
.tooltip-content {
position: relative;
display: flex;
.line-img {
width: 312.221px;
}
.line-img-left{
width: 216.86px;
margin-top: 30px;
}
.fill {
width: 9.165px;
height: 9.165px;
border-radius: 50%;
background-color: #18E4FF;
position: absolute;
left: 307.5px;
top: 77.5px;
z-index: 99;
}
.fill-left {
width: 9.165px;
height: 9.165px;
border-radius: 50%;
background-color: #18E4FF;
position: absolute;
right:210.5px;
top: 82.5px;
z-index: 99;
}
}
// // 轮播项目
// .carousel-item {
// flex: 0 0 auto;
// }
// // View Transitions 自定义样式
// @keyframes slide-from-right {
// from {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-left {
// to {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-from-left {
// from {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-right {
// to {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// // 自定义 View Transitions 动画
// ::view-transition-old(web3-item-1-4),
// ::view-transition-old(web3-item-2-4) {
// animation: 0.8s slide-to-left ease-in-out;
// }
// ::view-transition-new(web3-item-1-0),
// ::view-transition-new(web3-item-2-0) {
// animation: 0.8s slide-from-left ease-in-out;
// }
// // 确保过渡期间元素可见
// ::view-transition-group(*) {
// animation-duration: 0.8s;
// }

View File

@ -1,6 +1,4 @@
import { useState } from "react";
import { useSelector } from "react-redux";
import { WorldGeo } from "./components/world-geo";
import NetflixSvg from "@/assets/svg/anti-forensics-forwarding/Netflix.svg?react";
import NetflixActiveSvg from "@/assets/svg/anti-forensics-forwarding/NetflixActive.svg?react";
@ -28,18 +26,20 @@ import YouTubeSvg from "@/assets/svg/anti-forensics-forwarding/YouTube.svg?react
import YouTubeActiveSvg from "@/assets/svg/anti-forensics-forwarding/YouTubeActive.svg?react";
import FacebookSvg from "@/assets/svg/anti-forensics-forwarding/Facebook.svg?react";
import FacebookActiveSvg from "@/assets/svg/anti-forensics-forwarding/FacebookActive.svg?react";
import { RootState } from "@/store";
import {
TRAFFIC_OBFUSCATION,
NESTED_ENCRYPTION,
DYNAMIC_ROUTE_GENERATOR,
APP_DIVERSION,
PASS_AUTHENTICATION,
} from "./data/mockData";
import "./index.scss";
import { App } from "antd";
import {
getDynamicRouteGeneration,
getNestedEncryption,
getPassAuthentication,
} from "@/api/flying-line";
import { WorldGeo } from "./components/world-geo";
export const DIALOGTYPE = {
ADDNode: {
@ -153,95 +153,83 @@ export const CONST_TOOLTIP_TYPE = {
type: "APP_DIVERSION",
title: "应用分流",
},
// 通认证
// 通认证
PASS_AUTHENTICATION: {
type: "PASS_AUTHENTICATION",
title: "通认证",
title: "通认证",
},
};
const DecentralizedElasticNetwork = () => {
const { proxy_info, path_list, newHomeProxies } = useSelector(
(state: RootState) => state.web3Reducer
);
const [tooltipType, setTooltipType] = useState(
CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type
);
const [tooltipClosed, setTooltipClosed] = useState(false);
const [selectedApp, setSelectedApp] = useState<any>(null);
const appDiversion = useMemo(() => {
return Apps.map((item) => {
const findApp = APP_DIVERSION.find(
(appItem) => item.name === appItem.name
);
return {
...item,
...findApp,
};
});
}, []);
const currentValue = useMemo(() => {
let value = null;
switch (tooltipType) {
case CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type:
value = [NESTED_ENCRYPTION];
break;
case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type:
value = [TRAFFIC_OBFUSCATION];
break;
case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type:
value = DYNAMIC_ROUTE_GENERATOR
break;
case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
value = selectedApp ? [selectedApp] : []
break;
case CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type:
value = [PASS_AUTHENTICATION]
break;
default:
break;
}
return value;
}, [tooltipType,selectedApp]);
const [dataInfo, setDataInfo] = useState<any>({
passAuthentication: [
{
...PASS_AUTHENTICATION,
},
],
nestedEncryption: [NESTED_ENCRYPTION],
dynamicRouteGeneration: DYNAMIC_ROUTE_GENERATOR,
});
let istrue = useRef(false);
const [nestedEncryption, setNestedEncryption] = useState<any>([]);
const [dynamicRouteGeneration, setDynamicRouteGeneration] = useState<any>([]);
const [logs, setLogs] = useState<any>([]);
const handleClickApp = (item: any) => {
console.log("item", item);
setSelectedApp(item);
const initData = async () => {
try {
const passAuthentication = await getPassAuthentication();
const dynamicRouteGeneration = await getDynamicRouteGeneration();
const nestedEncryption = await getNestedEncryption();
nestedEncryption.data.isLine = false;
dynamicRouteGeneration.data[0].isLine = false;
setNestedEncryption([nestedEncryption.data]);
setLogs(nestedEncryption.logs);
setDynamicRouteGeneration(dynamicRouteGeneration.data);
setDataInfo({
nestedEncryption: [nestedEncryption.data],
passAuthentication: [passAuthentication.data],
dynamicRouteGeneration: dynamicRouteGeneration.data,
});
} catch (error) {}
};
const getNewDynamicRouteGeneration = async () => {
const res = await getDynamicRouteGeneration();
res.data[0].isLine = istrue.current;
setDynamicRouteGeneration([...res.data]);
};
useEffect(()=>{
()=>{
setTooltipClosed(false);
}
},[])
let timer: any = null;
useEffect(() => {
timer = setInterval(() => {
getNewDynamicRouteGeneration();
}, 5000);
return () => {
clearInterval(timer);
};
}, [dynamicRouteGeneration]);
useEffect(() => {
initData();
() => {
setTooltipClosed(false);
};
}, []);
return (
<div className="decentralized w-full h-full flex flex-col relative">
<div className="flex items-center gap-[60px] absolute top-12 left-12 z-10">
{tooltipType === CONST_TOOLTIP_TYPE.APP_DIVERSION.type && appDiversion.map((item) => {
return (
<div
key={item.name}
className="flex items-center justify-center w-16 h-16 relative rounded-[4.95px] shadow-[0px_0px_3.299999952316284px_0px_rgba(84,87,255,1.00)] outline outline-[0.50px] outline-offset-[-0.50px] outline-indigo-50/60 overflow-hidden"
onClick={() => handleClickApp(item)}
>
{selectedApp?.name === item?.name ? (
<item.activeIcon />
) : (
<item.icon />
)}
</div>
);
})}
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo
currentValue={currentValue}
newHomeProxies={newHomeProxies}
selectedApp={selectedApp}
logs={logs}
nestedEncryption={nestedEncryption}
passAuthentication={dataInfo.passAuthentication}
dynamicRouteGeneration={dynamicRouteGeneration}
tooltipType={tooltipType}
tooltipClosed={tooltipClosed}
setTooltipClosed={setTooltipClosed}
@ -254,7 +242,7 @@ const DecentralizedElasticNetwork = () => {
setTooltipType(CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type);
}}
>
</div>
<div
@ -262,6 +250,12 @@ const DecentralizedElasticNetwork = () => {
onClick={() => {
setTooltipType(CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type);
setTooltipClosed(true);
dynamicRouteGeneration[0].isLine = false;
istrue.current = false;
setDynamicRouteGeneration([...dynamicRouteGeneration]);
nestedEncryption[0].isLine = true;
setNestedEncryption([...nestedEncryption]);
}}
>
@ -270,11 +264,18 @@ const DecentralizedElasticNetwork = () => {
className="bt1 cursor-pointer"
onClick={() => {
setTooltipType(CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type);
nestedEncryption[0].isLine = false;
setNestedEncryption([...nestedEncryption]);
setTooltipClosed(false);
dynamicRouteGeneration[0].isLine = true;
istrue.current = true;
setDynamicRouteGeneration([...dynamicRouteGeneration]);
}}
>
</div>
<div
{/* <div
className="bt1 cursor-pointer"
onClick={() => {
setTooltipType(CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type);
@ -290,7 +291,7 @@ const DecentralizedElasticNetwork = () => {
}}
>
</div>
</div> */}
</div>
</div>
);

View File

@ -3,178 +3,150 @@ import { FormInstance } from "antd";
import { FormDialog } from "@/components/FormDialog";
import { useDispatch, useSelector } from "react-redux";
import { nodeList,getRandomNodes } from "@/store/datas";
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 {
setClearFailTimer,
setClearWarningTimer,
} from "@/store/web3Slice";
import { AppDispatch, RootState } from "@/store";
import { isTimestampPlusTenMinutesBeforeNow } from "@/utils/tools";
import { removeMaliciousNodeList, removeNodeDownList } from "@/store/web3Slice";
import { countryCodeMap } from "@/data";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
title: string;
desc: string;
successText: string;
}
export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = (
props
props
) => {
const { name, 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={name}
/>
</div>
</div>
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,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit,
}: {
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
}) => {
const dispatch = useDispatch<AppDispatch>();
const { clearWarningTimer, clearFailTimer } = 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) {
dispatch(setClearFailTimer(Date.now()));
} else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
dispatch(setClearWarningTimer(Date.now()));
}
// showDialog(false);
};
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));
eventBus.emit(eventTypes.NODE_REMOVE, item.name);
});
} else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
maliciousNodeList.forEach((item) => {
dispatch(removeMaliciousNodeList(item));
eventBus.emit(eventTypes.NODE_REMOVE, item.name);
});
}
// showDialog(false);
};
const proxyList = useMemo(() => {
const newData = getRandomNodes(nodeList);
if (open) {
if (isClear) return [];
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
if (clearFailTimer) {
const clear =
isTimestampPlusTenMinutesBeforeNow(clearFailTimer);
if (clear) {
return newData
} else {
return [];
}
} else {
return newData;
}
}
if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
if (clearWarningTimer) {
const clear =
isTimestampPlusTenMinutesBeforeNow(clearWarningTimer);
console.log(clear,'clear')
if (clear) {
return newData
} else {
return [];
}
} else {
return newData;
}
}
const proxyList = useMemo(() => {
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
return nodeDownList;
} else {
return maliciousNodeList;
}
}, [nodeDownList, maliciousNodeList, type.title]);
return newData;
}
// 随机 2-9条 nodelist里面的数据
return [];
}, [nodeList, open, isClear, clearFailTimer, clearWarningTimer, type]);
useEffect(() => {
if (!open) {
setIsClear(false);
}
}, [open]);
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 />
)}
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.map((item) => {
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 className="text-lg font-medium text-zinc-950 leading-relaxed mt-5">
{type.title === NODEDIALOGTYPE.ClearFailNode.title
? "暂无掉线节点"
: "暂无恶意节点"}
</div>
</FormDialog>
);
</div>
)}
</div>
</FormDialog>
);
};

View File

@ -8,217 +8,217 @@ import { DIALOGTYPE } from "../../index";
import "./index.scss";
import { DefaultLink } from "./pathChoose";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
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,
onChange,
}: {
value?: { port: string; ip: string };
onChange?: (data: { port: string; ip: string }) => void;
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 [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 SwitchComponent = ({
value,
onChange,
}: {
value?: boolean;
onChange?: (data: boolean) => void;
}) => {
const [checked, setChecked] = useState(value);
return (
<div className="flex items-center">
<Switch
className="data-[state=checked]:bg-[#1E3A8A]"
checked={checked}
onCheckedChange={(e) => {
setChecked(e);
onChange?.(e);
}}
/>
<div className="ml-2 text-zinc-900 text-sm font-medium leading-tight">
</div>
</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">
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>
);
<Form.Item name="name" label="节点名称" rules={[{ required: true, message: '请输入节点名称' }]}>
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="节点名称"
/>
</Form.Item>
<Form.Item name="private_key" label="私钥" rules={[{ required: true, message: '请输入私钥' }]}>
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="私钥"
/>
</Form.Item>
<Form.Item name="public_key" label="公钥" rules={[{ required: true, message: '请输入公钥' }]}>
<Input
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
placeholder="公钥"
/>
</Form.Item>
<Form.Item name="ipAndPort" label="IP+端口" rules={[{ required: true, message: '请输入IP+端口' }]}>
<IpPortInput />
</Form.Item>
<Form.Item name="exit">
<SwitchComponent />
</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>
);
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,
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;
open: boolean;
setOpen: (open: boolean) => void;
successHandle: () => void;
dialogLoading: boolean;
canSubmit: boolean;
form: FormInstance;
type: DialogConfig;
handleSelectFile: () => void;
}) => {
const showDialog = (open: boolean) => {
setOpen(open);
};
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>
);
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,4 +1,4 @@
import { useEffect, useMemo, useRef, memo } from "react";
import { useEffect, useMemo, useRef, memo, useState } from "react";
import * as echarts from "echarts";
// import 'echarts-gl';
// import { useQueryClient } from "@tanstack/react-query";
@ -19,58 +19,9 @@ interface LinesItemType {
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 从国家经纬度数据中随机生成涟漪效果
// const createRandomCountryRipples = (count = 10) => {
// // 获取所有国家代码
// const countryCodes = Object.keys(geoCoordMap);
// // 创建随机点数据
// const randomPoints = [];
// // 随机选择国家
// for (let i = 0; i < count; i++) {
// // 随机选择一个国家代码
// const randomIndex = Math.floor(Math.random() * countryCodes.length);
// const countryCode = countryCodes[randomIndex];
// // 获取该国家的坐标
// const coords = geoCoordMap[countryCode];
// // 添加一些微小的随机偏移,使点不完全重叠在国家中心
// const offsetLon = (Math.random() - 0.5) * 5; // ±2.5度的经度偏移
// const offsetLat = (Math.random() - 0.5) * 5; // ±2.5度的纬度偏移
// randomPoints.push({
// name: countryCode,
// value: [coords[0] + offsetLon, coords[1] + offsetLat],
// // 可以随机设置涟漪大小
// symbolSize: 3 + Math.random() * 5,
// // 可以存储国家代码用于显示信息
// countryCode: countryCode,
// });
// }
// // 返回涟漪效果系列
// return {
// type: "effectScatter",
// coordinateSystem: "geo",
// zlevel: 2,
// symbol: "circle",
// // 使用数据中的symbolSize
// symbolSize: (data: any) => data.symbolSize || 5,
// rippleEffect: {
// period: 4 + Math.random() * 4, // 随机周期
// brushType: "stroke",
// scale: 4 + Math.random() * 3, // 随机大小
// },
// itemStyle: {
// color: "#0ea5e9",
// shadowBlur: 10,
// shadowColor: "#0ea5e9",
// },
// data: randomPoints,
// // 确保不显示标签
// label: {
// show: false,
// },
// // 确保不响应鼠标事件
// silent: true,
// } as echarts.SeriesOption;
// };
// 连线动画的间隔时间(毫秒)
const LINE_ANIMATION_INTERVAL = 500; // 3秒
// 创建单个国家的涟漪效果
const createCountryRipple = (countryCode: string) => {
@ -85,22 +36,101 @@ const createCountryRipple = (countryCode: string) => {
};
export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
// const queryClient = useQueryClient()
const proxyGeoRef = useRef<EChartsType | null>(null);
const preMainToData = useRef<{ country_code: string }[]>([]);
// 添加状态来跟踪当前显示的连线索引
const [currentLineIndex, setCurrentLineIndex] = useState(-1);
// 添加状态来存储所有连线数据
const [lineConnections, setLineConnections] = useState<{from: string, to: string}[]>([]);
// 添加状态来存储所有点
const [allPoints, setAllPoints] = useState<any[]>([]);
// 使用ref来跟踪动画状态避免重新渲染
const animationTimerRef = useRef<NodeJS.Timeout | null>(null);
// 添加状态来跟踪数据是否已经变化
const dataKeyRef = useRef<string>("");
// 添加状态来跟踪是否应该显示连线
const [shouldShowLines, setShouldShowLines] = useState(false);
// 监听 screenData.proxy_info.proxies[0].isLine 的变化
useEffect(() => {
const isLine = screenData?.proxy_info?.proxies?.[0]?.isLine;
setShouldShowLines(!!isLine);
// 如果 isLine 从 false 变为 true重置连线索引并启动动画
if (isLine && currentLineIndex === -1 && lineConnections.length > 0) {
// 清除任何现有的动画定时器
if (animationTimerRef.current) {
clearTimeout(animationTimerRef.current);
animationTimerRef.current = null;
}
// 启动连线动画
setTimeout(() => {
startLineAnimation(lineConnections);
}, 0);
}
// 如果 isLine 从 true 变为 false重置连线索引
if (!isLine && currentLineIndex !== -1) {
setCurrentLineIndex(-1);
// 清除任何现有的动画定时器
if (animationTimerRef.current) {
clearTimeout(animationTimerRef.current);
animationTimerRef.current = null;
}
}
}, [screenData?.proxy_info?.proxies]);
// 处理代理数据
const mainToData = useMemo(() => {
// 使用新的数据结构
const proxiesList = screenData?.proxy_info?.proxies ?? [{data:[{country_code: 'AI', ingress_country_code: 'AE'}], isLine: true}];
console.log(proxiesList,'proxiesList')
// 初始化数据数组 - 不再包含 startCountry
const proxiesList = screenData?.proxy_info?.proxies ?? [{data:[{country_code: 'AI', ingress_country_code: 'AE'}], isLine: false}];
// 初始化数据数组
const data: any = [];
console.log(proxiesList, 'proxiesList');
// 收集所有点和连线信息
const points: any[] = [];
const connections: {from: string, to: string}[] = [];
// 遍历代理列表
proxiesList.forEach((proxyItem: any) => {
// 检查是否有数据数组
if (proxyItem.data && Array.isArray(proxyItem.data)) {
// 遍历数据数组中的每个项目
proxyItem.data.forEach((item: any) => {
// 添加起点到点集合
const fromCode = item.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode);
if (fromPoint && !points.some(p => p.country_code === fromCode)) {
points.push(fromPoint);
}
// 如果有终点,也添加到点集合
if (item.ingress_country_code) {
const toCode = item.ingress_country_code.toUpperCase();
const toPoint = createCountryRipple(toCode);
if (toPoint && !points.some(p => p.country_code === toCode)) {
points.push(toPoint);
}
// 如果需要连线,添加到连线集合
if (proxyItem.isLine === true) {
connections.push({
from: fromCode,
to: toCode
});
}
}
// 如果有 ingress_country_code则添加一对起点和终点
if (item.ingress_country_code) {
// 添加起点country_code
@ -126,8 +156,66 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
}
});
// 更新点和连线状态
setAllPoints(points);
setLineConnections(connections);
// 生成当前数据的唯一键
const currentDataKey = JSON.stringify(proxiesList);
// 检查数据是否变化
if (currentDataKey !== dataKeyRef.current) {
// 数据变化,重置连线索引
setCurrentLineIndex(-1);
dataKeyRef.current = currentDataKey;
// 清除任何现有的动画定时器
if (animationTimerRef.current) {
clearTimeout(animationTimerRef.current);
animationTimerRef.current = null;
}
// 只有当 shouldShowLines 为 true 且有连线数据时才启动动画
if (shouldShowLines && connections.length > 0) {
setTimeout(() => {
startLineAnimation(connections);
}, 500); // 短暂延迟,确保点已经显示
}
}
return data;
}, [screenData]);
}, [screenData, shouldShowLines]);
// 启动连线动画的函数
const startLineAnimation = (connections: {from: string, to: string}[]) => {
if (connections.length === 0 || !shouldShowLines) return;
let index = 0;
// 递归函数,用于按顺序显示连线
const animateNextLine = () => {
setCurrentLineIndex(index);
index++;
if (index < connections.length) {
animationTimerRef.current = setTimeout(animateNextLine, LINE_ANIMATION_INTERVAL);
}
};
// 开始动画
animateNextLine();
};
// 组件卸载时清除定时器
useEffect(() => {
return () => {
if (animationTimerRef.current) {
clearTimeout(animationTimerRef.current);
animationTimerRef.current = null;
}
};
}, []);
const getLineItem = (
preCode: string,
@ -151,55 +239,12 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
// 实现数据处理
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
const ripplePoints: any[] = [];
// 处理主路径数据
for (let i = 0; i < mainToData.length; i++) {
// 如果是最后一个元素,则跳过(因为没有下一个元素作为终点)
if (i === mainToData.length - 1) continue;
const currentItem = mainToData[i];
const nextItem = mainToData[i + 1];
// 获取当前国家代码
const countryCode = currentItem.country_code.toUpperCase();
// 如果当前项是起点,下一项是终点
if (currentItem.type === "start" && nextItem.type === "end") {
const startCode = countryCode;
const endCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const startPoint = createCountryRipple(startCode);
const endPoint = createCountryRipple(endCode);
if (startPoint) ripplePoints.push(startPoint);
if (endPoint) ripplePoints.push(endPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
solidData[0]?.[1].push(getLineItem(startCode, endCode));
}
// 跳过下一项,因为已经处理了
i++;
}
// 常规情况:当前项到下一项
else {
const nextCountryCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const currentPoint = createCountryRipple(countryCode);
const nextPoint = createCountryRipple(nextCountryCode);
if (currentPoint) ripplePoints.push(currentPoint);
if (nextPoint) ripplePoints.push(nextPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
solidData[0]?.[1].push(
getLineItem(countryCode, nextCountryCode)
);
}
// 只有当 shouldShowLines 为 true 时才显示连线
if (shouldShowLines) {
// 只显示到当前索引的连线
for (let i = 0; i <= currentLineIndex && i < lineConnections.length; i++) {
const connection = lineConnections[i];
solidData[0]?.[1].push(getLineItem(connection.from, connection.to));
}
}
@ -213,7 +258,7 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
return {
solidData,
otherLineList,
ripplePoints
ripplePoints: allPoints // 使用所有点,无论是否连线
};
};
@ -349,7 +394,7 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
const getLianData = (series: echarts.SeriesOption[]) => {
const { solidData, otherLineList, ripplePoints } = getLine();
// 如果有需要显示涟漪效果但不连线的点,添加它们
// 添加所有点的涟漪效果,无论是否连线
if (ripplePoints.length > 0) {
// 添加外层蓝色点,带涟漪效果
series.push({
@ -398,48 +443,53 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
} as echarts.SeriesOption);
}
solidData.forEach((item) => {
// 如果没有连线数据,则跳过
if (item[1].length === 0) {
return;
}
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 添加飞行线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false,
},
// 飞行线特效
effect: {
show: true, // 是否显示
period: 4, // 特效动画时间
trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长
symbol: planePathImg, // 特效图形标记
symbolSize: [10, 20],
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: "solid", // 飞线类型
color: "#0ea5e9", // 飞线颜色
width: 1.5, // 飞线宽度
opacity: 0.1,
},
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
// 只有当 shouldShowLines 为 true 时才添加连线
if (shouldShowLines) {
solidData.forEach((item) => {
// 如果没有连线数据,则跳过
if (item[1].length === 0) {
return;
}
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 添加飞行线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false,
},
// 飞行线特效
effect: {
show: true, // 是否显示
period: 4, // 特效动画时间
trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长
// symbol: planePathImg, // 特效图形标记
color:"#0ea5e9",
symbolSize: [10, 20],
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: "solid", // 飞线类型
color: "#0ea5e9", // 飞线颜色
width: 1.5, // 飞线宽度
opacity: 0.1,
},
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true);
series.push(...pathPoints);
// 添加出口节点的双层效果
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, true);
series.push(...exitNodes);
}
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true);
series.push(...pathPoints);
// 添加出口节点的双层效果
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, true);
series.push(...exitNodes);
}
});
}
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
@ -589,11 +639,6 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
getMianLineTipData(series);
// 添加随机涟漪效果 - 可以添加多组不同参数的涟漪
// series.push(createRandomCountryRipples(15)); // 添加15个随机涟漪点
// 可以添加第二组不同参数的涟漪
// const secondRippleEffect = createRandomCountryRipples(20);
// series.push(secondRippleEffect); // 添加10个随机涟漪点
const regions = getRegions();
const option = {
backgroundColor: "transparent",
@ -665,6 +710,13 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
const handleResize = () => {
proxyGeoRef.current?.resize();
};
// 更新图表
useEffect(() => {
const option = getOption();
proxyGeoRef.current?.setOption(option);
}, [currentLineIndex, shouldShowLines]); // 当当前连线索引或shouldShowLines变化时更新图表
useEffect(() => {
preMainToData.current?.some(
(item, index) =>
@ -674,6 +726,7 @@ export const WorldGeo = memo(({ screenData }: { screenData: any }) => {
const option = getOption();
proxyGeoRef.current?.setOption(option);
}, [screenData, mainToData]);
useEffect(() => {
const chartDom = document.getElementById("screenGeo");
proxyGeoRef.current = echarts.init(chartDom);

View File

@ -4,7 +4,7 @@ export const screenData = {
account: "admin",
account_is_admin: true,
exclusive: "none",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
proxies: ["意大利-米兰-312", "南苏丹-朱巴-374"],
proxies_code: ["it", "ss"],
use: true,
@ -14,7 +14,7 @@ export const screenData = {
proxy_info: {
exclusive: "",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
wg: false,
change_time: 0,
change_at: 0,

View File

@ -1,8 +1,12 @@
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 { toast } from "@/components/ui/use-toast";
import { errorToast } from "@/components/GlobalToast";
import { WorldGeo } from "./components/world-geo";
import Web3BoxPng from "@/assets/image/home/web3-box.png";
import Web3Box2Png from "@/assets/image/home/web3-box2.png";
@ -12,286 +16,301 @@ 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 {
setProxyInfoProxies,
setProxiesList1,
setProxiesList2,
setProxiesLine,
} from "@/store/web3Slice";
import eventBus, { eventTypes } from "@/utils/eventBus";
import { setProxyInfoProxies, setProxiesLine } from "@/store/web3Slice";
import type { AppDispatch, RootState } from "@/store";
import "./index.scss";
import { DialogConfig, FormAlertDialog } from "./components/FormAlertDialog";
import { ClearNodeDialog } from "./components/ClearNodeDialog";
export const DIALOGTYPE = {
ADDNode: {
title: "添加节点",
desc: "",
successText: "添加",
},
AddNetwork: {
title: "构建网络",
desc: "",
successText: "构建",
},
ADDNode: {
title: "添加节点",
desc: "",
successText: "添加",
},
AddNetwork: {
title: "构建网络",
desc: "",
successText: "构建",
},
};
export const NODEDIALOGTYPE = {
ClearFailNode: {
title: "清除掉线节点",
desc: "",
successText: "清除",
},
ClearWargingNode: {
title: "恶意节点",
desc: "",
successText: "清除",
},
ClearFailNode: {
title: "清除掉线节点",
desc: "",
successText: "清除",
},
ClearWargingNode: {
title: "恶意节点",
desc: "",
successText: "清除",
},
};
const DecentralizedElasticNetwork = () => {
const dispatch = useDispatch<AppDispatch>();
const { web3List, web3List2, proxy_info, path_list } = useSelector(
(state: RootState) => state.web3Reducer
);
const dispatch = useDispatch<AppDispatch>();
const { web3List, web3List2, proxy_info, path_list } = useSelector(
(state: RootState) => state.web3Reducer
);
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [openNode, setOpenNode] = useState(false);
const [dialogLoading] = useState(false);
const [type, setType] = useState<DialogConfig>(DIALOGTYPE.ADDNode);
const [nodeType, setNodeType] = useState<DialogConfig>(
NODEDIALOGTYPE.ClearFailNode
);
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [openNode, setOpenNode] = useState(false);
const [dialogLoading] = useState(false);
const [type, setType] = useState<DialogConfig>(DIALOGTYPE.ADDNode);
const [nodeType, setNodeType] = useState<DialogConfig>(
NODEDIALOGTYPE.ClearFailNode
);
const newWeb3List = useMemo(() => {
// 展示最新的6个节点
return web3List.slice(-6);
}, [web3List]);
const newWeb3List = useMemo(() => {
// 展示最新的6个节点
return web3List.slice(-6);
}, [web3List]);
const successHandle = async () => {
await form.validateFields();
const formValue: any = form.getFieldsValue();
if (type.title === DIALOGTYPE.ADDNode.title) {
setOpen(false);
} else {
const { inbound, outbound } = formValue || {};
if (inbound && outbound) {
dispatch(
setProxyInfoProxies({
country_code: inbound,
ingress_country_code: outbound,
})
);
setOpen(false);
}
}
};
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 successHandle = async () => {
await form.validateFields();
const formValue: any = form.getFieldsValue();
if (type.title === DIALOGTYPE.ADDNode.title) {
eventBus.emit(eventTypes.NODE_INIT, {});
setOpen(false);
} else {
const { inbound, outbound } = formValue || {};
if (inbound && outbound) {
dispatch(
setProxyInfoProxies({
country_code: inbound,
ingress_country_code: outbound,
})
);
setOpen(false);
}
}
};
const ICircuitRequest = useMemo(() => {
return {
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit: true,
handleSelectFile,
};
}, [open, dialogLoading, type, handleSelectFile]);
async function handleSelectFile() {
try {
const selected = await openFile({
multiple: false,
directory: false,
filters: [
{
name: "Excel Files",
extensions: ["xlsx", "xls"], // 移除了扩展名前的点号
},
],
});
if (selected && typeof selected === "string") {
const data = await readFile(selected);
// 将二进制数据转换为 ArrayBuffer
const arrayBuffer = data.buffer;
// 使用 XLSX 库解析 Excel 文件
const workbook = XLSX.read(arrayBuffer, { type: "array" });
const nodeSuccessHandle = () => {};
// 获取第一个工作表
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const ClearNodeDialogProps = useMemo(() => {
return {
open: openNode,
setOpen: setOpenNode,
successHandle: nodeSuccessHandle,
dialogLoading,
form,
type: nodeType,
canSubmit: true,
};
}, [openNode, dialogLoading, nodeType]);
// 将工作表转换为 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_INIT, item);
});
setOpen(false);
return;
} else {
errorToast("Excel 文件为空或格式不正确", toast);
}
}
}
} catch (err) {
console.error("Error selecting file:", err);
}
}
const screenData = useMemo(() => {
return {
path_list,
proxy_info,
};
}, [path_list, proxy_info]);
const ICircuitRequest = useMemo(() => {
return {
open,
setOpen,
successHandle,
dialogLoading,
form,
type,
canSubmit: true,
handleSelectFile,
};
}, [open, dialogLoading, type, handleSelectFile]);
// useEffect(() => {
// dispatch(randomUpdateWeb3List());
// dispatch(randomUpdateWeb3List2());
// // 每1.5秒更新一次
// const interval = setInterval(() => {
// dispatch(randomUpdateWeb3List());
// dispatch(randomUpdateWeb3List2());
// }, 1500);
const nodeSuccessHandle = () => {};
// return () => clearInterval(interval);
// }, [dispatch]);
const ClearNodeDialogProps = useMemo(() => {
return {
open: openNode,
setOpen: setOpenNode,
successHandle: nodeSuccessHandle,
dialogLoading,
form,
type: nodeType,
canSubmit: true,
};
}, [openNode, dialogLoading, nodeType]);
return (
<div className="decentralized w-full h-full flex flex-col relative">
<div className="box"></div>
<div className="w-full flex items-center justify-center relative">
{/* <img
const screenData = useMemo(() => {
return {
path_list,
proxy_info,
};
}, [path_list, proxy_info]);
// useEffect(() => {
// dispatch(randomUpdateWeb3List());
// dispatch(randomUpdateWeb3List2());
// // 每1.5秒更新一次
// const interval = setInterval(() => {
// dispatch(randomUpdateWeb3List());
// dispatch(randomUpdateWeb3List2());
// }, 1500);
// return () => clearInterval(interval);
// }, [dispatch]);
useEffect(()=>{
return ()=>{
dispatch(setProxiesLine(false));
}
},[])
return (
<div className="decentralized w-full h-full flex flex-col relative">
<div className="box"></div>
<div className="w-full flex items-center justify-center relative">
{/* <img
className="w-[1693px] h-[271px] absolute top-[160px] left-[50%] translate-x-[-50%] z-10"
src={linePng}
alt=""
/> */}
{/* <div className="w-[90%] absolute top-0 left-0"></div> */}
<div className="w-[1693px] h-full flex items-center justify-center">
<div className="w-[795px] h-full flex items-center justify-end gap-10 z-10">
<div className="carousel-container !justify-end">
{newWeb3List.map((item, index) => {
// 随机0-10的整数
const randomDelay =
Math.floor(Math.random() * 35) * 1;
return (
<div
className="w-[105px] relative carousel-item"
key={`${item.id}-${index}`}
style={{
viewTransitionName: `web3-item-1-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className={cn(
"!max-w-[186px] h-[160px] relative opacity-50 mix-blend-soft-light z-10"
)}
style={{
left: `${
-30 - randomDelay
}px`,
top: `${
-30 - randomDelay
}px`,
}}
src={web3BoxGif}
/>
</div>
<img
src={Web3Box2Png}
className="w-full h-full"
/>
<div className="absolute bottom-[-170px] left-[55px] h-[160px] w-[2px] bg-gradient-to-bl to-[#4820C9]/0 from-[#7D82FF] web3-line"></div>
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-white pl-1.5 z-20">
{/* <div className="justify-start text-pink-600 text-2xl font-normal font-['Oswald'] absolute top-[-34px] left-[50%] translate-x-[-50%]">
{/* <div className="w-[90%] absolute top-0 left-0"></div> */}
<div className="w-[1693px] h-full flex items-center justify-center">
<div className="w-[795px] h-full flex items-center justify-end gap-10 z-10">
<div className="carousel-container !justify-end">
{newWeb3List.map((item, index) => {
// 随机0-10的整数
const randomDelay = Math.floor(Math.random() * 35) * 1;
return (
<div
className="w-[105px] relative carousel-item"
key={`${item.id}-${index}`}
style={{
viewTransitionName: `web3-item-1-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className={cn(
"!max-w-[186px] h-[160px] relative opacity-50 mix-blend-soft-light z-10"
)}
style={{
left: `${-30 - randomDelay}px`,
top: `${-30 - randomDelay}px`,
}}
src={web3BoxGif}
/>
</div>
<img src={Web3Box2Png} className="w-full h-full" />
{/* <div className="absolute bottom-[-170px] left-[55px] h-[160px] w-[2px] bg-gradient-to-bl to-[#4820C9]/0 from-[#7D82FF] web3-line"></div> */}
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-white pl-1.5 z-20">
{/* <div className="justify-start text-pink-600 text-2xl font-normal font-['Oswald'] absolute top-[-34px] left-[50%] translate-x-[-50%]">
{item.transactions}
</div> */}
<div className="text-lg">
{item.balance} SOL
</div>
<div className="!text-xs my-[10px]">
{item.numberTransactions}
</div>
<div className="!text-sm opacity-60">
{item.upDatedAt}
</div>
</div>
</div>
);
})}
</div>
{/* <div className="!text-xs">{item?.balanceToFixed} SOL</div> */}
<div className="!text-xs my-[6px]">#{item.height}</div>
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">
{item.txs.length}
</div>
</div>
<div className="w-fit mt-6 mx-[20px] flex-shrink-0">
<VectorSlideSvg />
</div>
<div className="w-[795px] h-full flex items-center justify-start gap-10 ">
<div className="carousel-container justify-start">
{web3List2.map((item, index) => {
const randomDelay =
Math.floor(Math.random() * 35) * 1;
return (
<div
key={`${item.id}-${index}`}
className="w-[105px] relative carousel-item"
style={{
viewTransitionName: `web3-item-2-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className="!max-w-[186px] h-[160px] relative left-[-30px] top-[-30px] opacity-50 mix-blend-soft-light z-10"
src={web3BoxGif}
/>
</div>
<img
src={Web3BoxPng}
style={{
left: `${-30 - randomDelay}px`,
top: `${-30 - randomDelay}px`,
}}
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]">
{item.numberTransactions}
</div>
<div className="!text-sm opacity-60">
{item.upDatedAt}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo screenData={screenData} />
</div>
<div className="absolute bottom-6 left-[50%] translate-x-[-50%] w-[calc(100%-51px)] p-6 bg-indigo-950 bg-opacity-10 rounded-md outline outline-1 outline-zinc-200 outline-opacity-40 backdrop-blur-lg inline-flex justify-start items-center gap-4">
<div
className="bt1 cursor-pointer"
onClick={() => {
setType(DIALOGTYPE.ADDNode);
setOpen(true);
</div>
<div className="w-fit mt-6 mx-[20px] flex-shrink-0">
<VectorSlideSvg />
</div>
<div className="w-[795px] h-full flex items-center justify-start gap-10 ">
<div className="carousel-container justify-start">
{web3List2.map((item, index) => {
const randomDelay = Math.floor(Math.random() * 35) * 1;
return (
<div
key={`${item.id}-${index}`}
className="w-[105px] relative carousel-item"
style={{
viewTransitionName: `web3-item-2-${index}`,
}}
>
<AddSvg />
</div>
{/* <div
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className="!max-w-[186px] h-[160px] relative left-[-30px] top-[-30px] opacity-50 mix-blend-soft-light z-10"
src={web3BoxGif}
/>
</div>
<img
src={Web3BoxPng}
style={{
left: `${-30 - randomDelay}px`,
top: `${-30 - randomDelay}px`,
}}
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-xs my-[6px]">#{item.height}</div>
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">
{item.numberTransactions}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo screenData={screenData} />
</div>
<div className="absolute bottom-6 left-[50%] translate-x-[-50%] w-[calc(100%-51px)] p-6 bg-indigo-950 bg-opacity-10 rounded-md outline outline-1 outline-zinc-200 outline-opacity-40 backdrop-blur-lg inline-flex justify-start items-center gap-4">
<div
className="bt1 cursor-pointer"
onClick={() => {
setType(DIALOGTYPE.ADDNode);
setOpen(true);
}}
>
<AddSvg />
</div>
{/* <div
className="bt1 cursor-pointer"
onClick={() => {
setType(DIALOGTYPE.AddNetwork);
@ -301,40 +320,41 @@ const DecentralizedElasticNetwork = () => {
<InterSvg />
</div> */}
<div
className="bt1 cursor-pointer"
onClick={() => {
dispatch(setProxiesLine());
}}
>
<InterSvg />
</div>
<div
className="bt2 cursor-pointer"
onClick={() => {
setNodeType(NODEDIALOGTYPE.ClearWargingNode);
setOpenNode(true);
}}
>
</div>
<div
className="bt2 cursor-pointer"
onClick={() => {
setNodeType(NODEDIALOGTYPE.ClearFailNode);
setOpenNode(true);
}}
>
<TrashSvg />
线
</div>
<div></div>
</div>
<FormAlertDialog {...ICircuitRequest} />
<ClearNodeDialog {...ClearNodeDialogProps} />
<div
className="bt1 cursor-pointer"
onClick={() => {
console.log(proxy_info,'proxy_infoproxy_info')
dispatch(setProxiesLine(true));
}}
>
<InterSvg />
</div>
);
<div
className="bt2 cursor-pointer"
onClick={() => {
setNodeType(NODEDIALOGTYPE.ClearWargingNode);
setOpenNode(true);
}}
>
</div>
<div
className="bt2 cursor-pointer"
onClick={() => {
setNodeType(NODEDIALOGTYPE.ClearFailNode);
setOpenNode(true);
}}
>
<TrashSvg />
线
</div>
<div></div>
</div>
<FormAlertDialog {...ICircuitRequest} />
<ClearNodeDialog {...ClearNodeDialogProps} />
</div>
);
};
export default DecentralizedElasticNetwork;

View File

@ -15,7 +15,6 @@ import {
enableProxy,
disableProxy,
} from '@/store/serviceSlice'
import { createCircuit } from '@/store/circuitSlice'
import { WebSocketClient } from '@/utils/webSocketClient'
import { ServiceControlPanel } from '@/components/ServiceControlPanel'
import { useCoreConfig } from '@/hooks/useCoreConfig'
@ -181,20 +180,20 @@ function Home() {
}
}
// 如果代理未启用且链路未就绪,创建默认链路
if (!isProxyEnabled && !isCircuitReady) {
await dispatch(
createCircuit({
uid: uuidv4(),
name: '系统默认链路',
inbound: countries[0],
outbound: exitCountries[exitCountries.length - 1],
multi_hop: 3,
fallback: true,
rule_path: null,
is_prefix: false,
}),
).unwrap()
}
// if (!isProxyEnabled && !isCircuitReady) {
// await dispatch(
// createCircuit({
// uid: uuidv4(),
// name: '系统默认链路',
// inbound: countries[0],
// outbound: exitCountries[exitCountries.length - 1],
// multi_hop: 3,
// fallback: true,
// rule_path: null,
// is_prefix: false,
// }),
// ).unwrap()
// }
setIsProxyLoading(true)

View File

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

View File

@ -1,180 +0,0 @@
import { FormInstance } from "antd";
import { FormDialog } from "@/components/FormDialog";
import { useDispatch, useSelector } from "react-redux";
import { nodeList,getRandomNodes } from "@/store/datas";
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 {
setClearFailTimer,
setClearWarningTimer,
} from "@/store/web3Slice";
import { AppDispatch, RootState } from "@/store";
import { isTimestampPlusTenMinutesBeforeNow } from "@/utils/tools";
export interface DialogConfig {
title: string;
desc: string;
successText: string;
}
export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = (
props
) => {
const { name, 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={name}
/>
</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 { clearWarningTimer, clearFailTimer } = 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) {
dispatch(setClearFailTimer(Date.now()));
} else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
dispatch(setClearWarningTimer(Date.now()));
}
// showDialog(false);
};
const proxyList = useMemo(() => {
const newData = getRandomNodes(nodeList);
if (open) {
if (isClear) return [];
if (type.title === NODEDIALOGTYPE.ClearFailNode.title) {
if (clearFailTimer) {
const clear =
isTimestampPlusTenMinutesBeforeNow(clearFailTimer);
if (clear) {
return newData
} else {
return [];
}
} else {
return newData;
}
}
if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) {
if (clearWarningTimer) {
const clear =
isTimestampPlusTenMinutesBeforeNow(clearWarningTimer);
console.log(clear,'clear')
if (clear) {
return newData
} else {
return [];
}
} else {
return newData;
}
}
return newData;
}
// 随机 2-9条 nodelist里面的数据
return [];
}, [nodeList, open, isClear, clearFailTimer, clearWarningTimer, type]);
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.map((item) => {
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>
// </>
// )
// }

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ export const screenData = {
account: "admin",
account_is_admin: true,
exclusive: "none",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
proxies: ["意大利-米兰-312", "南苏丹-朱巴-374"],
proxies_code: ["it", "ss"],
use: true,
@ -14,7 +14,7 @@ export const screenData = {
proxy_info: {
exclusive: "",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
wg: false,
change_time: 0,
change_at: 0,

View File

@ -1,170 +0,0 @@
// // 添加到 index.scss
.decentralized {
background-color: #0f172a;
// background-image: linear-gradient(180deg, #172554 0%, #0A0F2A 100%);
.box {
width: 100%;
height: 100%;
background-image: url("@/assets/image/line-bg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
position: absolute;
left: 0;
top: 0;
mix-blend-mode: lighten;
}
.web3-line::after {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #7D82FF;
position: absolute;
left: 50%;
top: 0px;
z-index: 999;
transform: translate(-50%, 0);
}
}
// // 轮播容器样式
.carousel-container {
display: flex;
align-items: center;
gap: 3rem; // 对应原来的gap-12
// width: 100%;
}
.bt1 {
display: flex;
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
justify-content: center;
align-items: center;
gap: var(--8-spacing-04, 8px);
border-radius: var(--radius-6, 6px);
border: 1px solid var(--Colors-Bluepurple-600, #4136F5);
background: var(--button-wireframe-button-wireframe, rgba(9, 9, 11, 0.00));
box-shadow: 0px 0px 4px 0px var(--Colors-Bluepurple-500, #5457FF), 0px 0px 10px 0px var(--Colors-Bluepurple-600, #4136F5);
color: var(--text-text-primary-900, #FFF);
/* Text/Medium/T5文本1 */
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
// line-height: 24px;
/* 171.429% */
}
.bt2 {
display: flex;
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
justify-content: center;
align-items: center;
gap: var(--8-spacing-04, 8px);
border-radius: var(--radius-6, 6px);
border: 1px solid var(--Colors-Rose-600, #E11D48);
background: var(--button-wireframe-button-wireframe, rgba(255, 255, 255, 0.00));
box-shadow: 0px 0px 4px 0px var(--Colors-Rose-600, #E11D48), 0px 0px 10px 0px var(--Colors-Rose-600, #E11D48);
color: var(--text-text-primary-900, #FFF);
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
// line-height: 24px;
}
.tip-box {
// display: flex;
// flex-direction: column;
position: relative;
width: 626px;
height: 281px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
outline: 0.46px solid white;
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.close-icon {
width: 16px;
height: 16px;
position: absolute;
top: 20px;
right: 20px;
color: #FFF;
}
.label {
width: 100%;
color: #FFF;
font-size: 18px;
font-weight: 600;
line-height: 16px;
}
.encryption-img {
width: 526px;
height: 241px;
margin-left: 16px;
}
}
// // 轮播项目
// .carousel-item {
// flex: 0 0 auto;
// }
// // View Transitions 自定义样式
// @keyframes slide-from-right {
// from {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-left {
// to {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-from-left {
// from {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-right {
// to {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// // 自定义 View Transitions 动画
// ::view-transition-old(web3-item-1-4),
// ::view-transition-old(web3-item-2-4) {
// animation: 0.8s slide-to-left ease-in-out;
// }
// ::view-transition-new(web3-item-1-0),
// ::view-transition-new(web3-item-2-0) {
// animation: 0.8s slide-from-left ease-in-out;
// }
// // 确保过渡期间元素可见
// ::view-transition-group(*) {
// animation-duration: 0.8s;
// }

View File

@ -1,324 +1,328 @@
import { Form } from "antd";
import { open as openFile } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { WorldGeo } from "./components/world-geo";
import Web3BoxPng from "@/assets/image/home/web3-box.png";
import Web3Box2Png from "@/assets/image/home/web3-box2.png";
import OpenProxyPng from "@/assets/image/home/open-proxy.png";
import web3BoxGif from "@/assets/gif/web3-box-bg.gif";
import VectorSlideSvg from "@/assets/svg/home/vector-solide.svg?react";
import { Apps, CONST_TOOLTIP_TYPE } from "@/pages/anti-forensics-forwarding";
import { cn } from "@/lib/utils";
import { commands } from "@/bindings";
import {
setProxyInfoProxies,
setProxiesList1,
setProxiesList2,
} from "@/store/web3Slice";
import type { AppDispatch, RootState } from "@/store";
import "./index.scss";
import { DialogConfig, FormAlertDialog } from "./components/FormAlertDialog";
import { ClearNodeDialog } from "./components/ClearNodeDialog";
import { blockChainApi } from "@/api/block";
import { APP_DIVERSION } from "../anti-forensics-forwarding/data/mockData";
import {
getPassAuthentication,
getTrafficObfuscation,
getNestedEncryption,
getDynamicRouteGeneration,
getApplicationDiversion,
} from "@/api/flying-line";
import { errorToast } from "@/components/GlobalToast";
import { toast } from "@/components/ui/use-toast";
import { disableProxy, enableProxy } from "@/store/serviceSlice";
export const DIALOGTYPE = {
ADDNode: {
title: "添加节点",
desc: "",
successText: "添加",
},
AddNetwork: {
title: "构建网络",
desc: "",
successText: "构建",
},
ADDNode: {
title: "添加节点",
desc: "",
successText: "添加",
},
AddNetwork: {
title: "构建网络",
desc: "",
successText: "构建",
},
};
export const NODEDIALOGTYPE = {
ClearFailNode: {
title: "清除掉线节点",
desc: "",
successText: "清除",
},
ClearWargingNode: {
title: "恶意节点",
desc: "",
successText: "清除",
},
ClearFailNode: {
title: "清除掉线节点",
desc: "",
successText: "清除",
},
ClearWargingNode: {
title: "恶意节点",
desc: "",
successText: "清除",
},
};
const NewHome = () => {
const dispatch = useDispatch<AppDispatch>();
const { web3List, web3List2, newHomeProxies } = useSelector(
(state: RootState) => state.web3Reducer
);
const dispatch = useDispatch<AppDispatch>();
const { web3List, web3List2 } = useSelector(
(state: RootState) => state.web3Reducer
);
const { isProxyEnabled, isCoreRunning } = useSelector(
(state: RootState) => state.serviceReducer
);
const [isProxyLoading, setIsProxyLoading] = useState(false);
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
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 [selectedApp, setSelectedApp] = useState<any>(null);
const [blockChain, setBlockChain] = useState<any>(null);
const [tooltipClosed, setTooltipClosed] = useState(true);
const [tooltipClosed, setTooltipClosed] = useState(true);
const [tooltipType, setTooltipType] = useState(
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type
);
const [tooltipType, setTooltipType] = useState(
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type
);
// 模拟日志数据
const [nestedEncryptionLogs, setNestedEncryptionLogs] = useState<string[]>(
[]
);
const appDiversion = useMemo(() => {
return Apps.map((item) => {
const findApp = APP_DIVERSION.find(
(appItem) => item.name === appItem.name
);
return {
...item,
...findApp,
};
});
}, []);
const [trafficObfuscationLogs, setTrafficObfuscationLogs] = useState<
string[]
>([]);
const newWeb3List = useMemo(() => {
// 展示最新的6个节点
return web3List.slice(-6);
}, [web3List]);
const newWeb3List = useMemo(() => {
// 展示最新的6个节点
return web3List.slice(-6);
}, [web3List]);
const successHandle = async () => {
await form.validateFields();
const formValue: any = form.getFieldsValue();
if (type.title === DIALOGTYPE.ADDNode.title) {
setOpen(false);
} else {
const { inbound, outbound } = formValue || {};
if (inbound && outbound) {
dispatch(
setProxyInfoProxies({
country_code: inbound,
ingress_country_code: outbound,
})
);
setOpen(false);
}
const handleClickApp = (item: any) => {
setSelectedApp(item);
};
const [dataInfo, setDataInfo] = useState<any>({
passAuthentication: {
type: "PASS_AUTHENTICATION",
name: "通信认证",
startPoint: "GL",
endPoint: "CA",
authenticationPoint: [
[-103.346771, 54.130366],
[-120.346771, 52.130366],
[-108.346771, 48.130366],
[-98.346771, 46.130366],
[-106.346771, 48.450366],
[-101.346771, 53.130366],
[-123.346771, 58.130366],
[-111.346771, 65.443366],
[-108.346771, 54.130366],
[-116.346771, 59.130366],
[-97.346771, 61.130366],
[-95.346771, 63.130366],
[-113.346771, 58.840366],
[-99.346771, 59.130366],
[-102.346771, 68.130366],
],
data: [
{
country_code: "gl",
ingress_country_code: "dz",
},
{
country_code: "br",
ingress_country_code: "dz",
},
{
country_code: "dz",
ingress_country_code: "ru",
},
{
country_code: "dz",
ingress_country_code: "cn",
},
{
country_code: "ru",
ingress_country_code: "za",
},
],
isLine: true,
},
trafficObfuscation: [],
nestedEncryption: [],
dynamicRouteGeneration: [],
applicationDiversion: [],
});
const appDiversion = useMemo(() => {
return Apps.map((item) => {
const findApp = dataInfo.applicationDiversion.find(
(appItem: any) => item.name === appItem.name
);
return {
...item,
...findApp,
};
});
}, [dataInfo.applicationDiversion]);
// 处理代理开关
const handleProxyToggle = useCallback(
async (isProxyEnabled: boolean, isCoreRunning: boolean) => {
// console.log(isProxyLoading, "isProxyLoadingisProxyLoading");
if (isProxyLoading) return;
try {
// 如果核心未运行,先启动核心
if (!isCoreRunning) {
await commands.startCore();
}
};
const handleClickApp = (item: any) => {
setSelectedApp(item);
};
setIsProxyLoading(true);
// 切换代理状态
await dispatch(isProxyEnabled ? disableProxy() : enableProxy()).unwrap();
setIsProxyLoading(false);
console.log(`Proxy ${isProxyEnabled ? "关闭成功" : "开启成功"}`);
} catch (error) {
const errorMessage = isProxyEnabled
? "关闭代理失败!"
: "开启代理失败,请检查节点配置、或重新尝试开启";
errorToast(errorMessage, toast);
console.error("Proxy toggle failed:", error);
} finally {
setIsProxyLoading(false);
}
},
[dispatch, isProxyLoading, isCoreRunning, isProxyEnabled]
);
const initData = async () => {
const passAuthentication = await getPassAuthentication();
const trafficObfuscation = await getTrafficObfuscation();
const nestedEncryption = await getNestedEncryption();
const applicationDiversion = await getApplicationDiversion();
const dynamicRouteGeneration = await getDynamicRouteGeneration();
setNestedEncryptionLogs(nestedEncryption.logs);
setTrafficObfuscationLogs(trafficObfuscation.logs);
setDataInfo({
passAuthentication: passAuthentication.data,
trafficObfuscation: [trafficObfuscation.data],
nestedEncryption: [nestedEncryption.data],
applicationDiversion: applicationDiversion.data,
dynamicRouteGeneration: dynamicRouteGeneration.data,
});
};
useEffect(() => {
initData();
}, []);
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);
}
}
// useEffect(() => {
// console.log(dataInfo, "awaidataInfodataInfotawait");
// }, [dataInfo]);
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]);
// useEffect(() => {
// dispatch(randomUpdateWeb3List());
// dispatch(randomUpdateWeb3List2());
// // 每1.5秒更新一次
// const interval = setInterval(() => {
// dispatch(randomUpdateWeb3List());
// dispatch(randomUpdateWeb3List2());
// }, 1500);
// return () => clearInterval(interval);
// }, [dispatch]);
useEffect(()=>{
blockChainApi.getLatestBlock().then((res)=>{
console.log("res",res)
setBlockChain(res)
})
},[])
return (
<div className="decentralized w-full h-full flex flex-col relative">
<div className="box"></div>
<div className="w-full flex items-center justify-center relative">
{/* <img
return (
<div className="decentralized w-full h-full flex flex-col relative">
<div className="box"></div>
<div className="w-full flex items-center justify-center relative">
{/* <img
className="w-[1693px] h-[271px] absolute top-[160px] left-[50%] translate-x-[-50%] z-10"
src={linePng}
alt=""
/> */}
{/* <div className="w-[90%] absolute top-0 left-0"></div> */}
<div className="w-[1693px] h-full flex items-center justify-center absolute top-[90px]">
<div className="w-[795px] h-full flex items-center justify-end gap-10 z-10">
<div className="carousel-container !justify-end">
{newWeb3List.map((item, index) => {
// 随机0-10的整数
const randomDelay =
Math.floor(Math.random() * 35) * 1;
return (
<div
className="w-[105px] relative carousel-item"
key={`${item.id}-${index}`}
style={{
viewTransitionName: `web3-item-1-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className={cn(
"!max-w-[186px] h-[160px] relative opacity-50 mix-blend-soft-light z-10"
)}
style={{
left: `${
-30 - randomDelay
}px`,
top: `${
-30 - randomDelay
}px`,
}}
src={web3BoxGif}
/>
</div>
<img
src={Web3Box2Png}
className="w-full h-full"
/>
<div className="absolute bottom-[-170px] left-[55px] h-[160px] w-[2px] bg-gradient-to-bl to-[#4820C9]/0 from-[#7D82FF] web3-line"></div>
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-white pl-1.5 z-20">
{/* <div className="justify-start text-pink-600 text-2xl font-normal font-['Oswald'] absolute top-[-34px] left-[50%] translate-x-[-50%]">
{/* <div className="w-[90%] absolute top-0 left-0"></div> */}
<div className="w-[1693px] h-full flex items-center justify-center ">
<div className="w-[795px] flex items-center justify-end gap-10 z-[99]">
<div className="carousel-container !justify-end ">
{newWeb3List.map((item, index) => {
// 随机0-10的整数
const randomDelay = Math.floor(Math.random() * 35) * 1;
return (
<div
className="w-[105px] relative carousel-item"
key={`${item.id}-${index}`}
style={{
viewTransitionName: `web3-item-1-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className={cn(
"!max-w-[186px] h-[160px] relative opacity-50 mix-blend-soft-light z-10"
)}
style={{
left: `${-30 - randomDelay}px`,
top: `${-30 - randomDelay}px`,
}}
src={web3BoxGif}
/>
</div>
<img src={Web3Box2Png} className="w-full h-full" />
{/* <div className="absolute bottom-[-170px] left-[55px] h-[160px] w-[2px] bg-gradient-to-bl to-[#4820C9]/0 from-[#7D82FF] web3-line"></div> */}
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-white pl-1.5 z-20">
{/* <div className="justify-start text-pink-600 text-2xl font-normal font-['Oswald'] absolute top-[-34px] left-[50%] translate-x-[-50%]">
{item.transactions}
</div> */}
<div className="text-lg">
{item.balance} SOL
</div>
<div className="!text-xs my-[10px]">
{item.numberTransactions}
</div>
<div className="!text-sm opacity-60">
{item.upDatedAt}
</div>
</div>
</div>
);
})}
</div>
{/* <div className="!text-xs">{item?.balanceToFixed} SOL</div> */}
<div className="!text-xs my-[6px]">#{item.height}</div>
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">
{item.txs.length}
</div>
</div>
<div className="w-fit mt-6 mx-[20px] flex-shrink-0">
<VectorSlideSvg />
</div>
<div className="w-[795px] h-full flex items-center justify-start gap-10 ">
<div className="carousel-container justify-start">
{web3List2.map((item, index) => {
const randomDelay =
Math.floor(Math.random() * 35) * 1;
return (
<div
key={`${item.id}-${index}`}
className="w-[105px] relative carousel-item"
style={{
viewTransitionName: `web3-item-2-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className="!max-w-[186px] h-[160px] relative left-[-30px] top-[-30px] opacity-50 mix-blend-soft-light z-10"
src={web3BoxGif}
/>
</div>
<img
src={Web3BoxPng}
style={{
left: `${-30 - randomDelay}px`,
top: `${-30 - randomDelay}px`,
}}
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]">
{item.numberTransactions}
</div>
<div className="!text-sm opacity-60">
{item.upDatedAt}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo
newHomeProxies={newHomeProxies}
selectedApp={selectedApp}
tooltipType={tooltipType}
tooltipClosed={tooltipClosed}
setTooltipClosed={setTooltipClosed}
/>
</div>
<div className="w-fit mt-6 mx-[20px] flex-shrink-0">
<VectorSlideSvg />
</div>
<div className="w-[795px] h-full flex items-center justify-start gap-10 ">
<div className="carousel-container justify-start">
{web3List2.map((item, index) => {
const randomDelay = Math.floor(Math.random() * 35) * 1;
return (
<div
key={`${item.id}-${index}`}
className="w-[105px] relative carousel-item"
style={{
viewTransitionName: `web3-item-2-${index}`,
}}
>
<div className="w-[calc(100%-10px)] h-[calc(100%-10px)] absolute top-[6px] left-[8px] overflow-hidden">
<img
className="!max-w-[186px] h-[160px] relative left-[-30px] top-[-30px] opacity-50 mix-blend-soft-light z-10"
src={web3BoxGif}
/>
</div>
<img
src={Web3BoxPng}
style={{
left: `${-30 - randomDelay}px`,
top: `${-30 - randomDelay}px`,
}}
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-xs my-[6px]">#{item.height}</div>
<div className="!text-xs opacity-60 mb-[6px]">
{item.timerstamp}
</div>
<div className="!text-xs opacity-60">
{item.numberTransactions}
</div>
</div>
</div>
);
})}
</div>
<div className="absolute bottom-[10px] left-[50%] translate-x-[-50%] w-[calc(100%-51px)] p-6 inline-flex justify-start items-center gap-10">
<img src={OpenProxyPng} className="w-[193px] h-[90px] cursor-pointer" alt="" />
{/* <div
</div>
</div>
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo
dataInfo={dataInfo}
nestedEncryptionLogs={nestedEncryptionLogs}
trafficObfuscationLogs={trafficObfuscationLogs}
selectedApp={selectedApp}
tooltipType={tooltipType}
tooltipClosed={tooltipClosed}
setTooltipClosed={setTooltipClosed}
/>
</div>
<div className="absolute bottom-[10px] left-[50%] translate-x-[-50%] w-[calc(100%-51px)] p-6 inline-flex justify-start items-center gap-10">
<div
className="flex items-center justify-center w-[193px] h-[80px] cursor-pointer bg-[#1448F5] rounded-[40px] text-white text-lg font-medium"
onClick={() => {
handleProxyToggle(isProxyEnabled, isCoreRunning);
}}
>
{isProxyEnabled ? "关闭匿名服务" : "开启匿名服务"}
</div>
{/* <div
className="bt1 cursor-pointer"
onClick={() => {
setTooltipType(
@ -340,26 +344,24 @@ const NewHome = () => {
>
</div> */}
{appDiversion.map((item) => {
return (
<div
key={item.name}
className="flex items-center justify-center w-16 h-16 relative rounded-[4.95px] shadow-[0px_0px_3.299999952316284px_0px_rgba(84,87,255,1.00)] outline outline-[0.50px] outline-offset-[-0.50px] outline-indigo-50/60 overflow-hidden"
onClick={() => handleClickApp(item)}
>
{selectedApp?.name === item?.name ? (
<item.activeIcon />
) : (
<item.icon />
)}
</div>
);
})}
{appDiversion.map((item) => {
return (
<div
key={item.name}
className="flex items-center justify-center w-16 h-16 relative rounded-[4.95px] shadow-[0px_0px_3.299999952316284px_0px_rgba(84,87,255,1.00)] outline outline-[0.50px] outline-offset-[-0.50px] outline-indigo-50/60 overflow-hidden"
onClick={() => handleClickApp(item)}
>
{selectedApp?.name === item?.name ? (
<item.activeIcon />
) : (
<item.icon />
)}
</div>
<FormAlertDialog {...ICircuitRequest} />
<ClearNodeDialog {...ClearNodeDialogProps} />
</div>
);
);
})}
</div>
</div>
);
};
export default NewHome;

View File

@ -1,39 +1,5 @@
.proxies {
// .proxies-container {
// &_country {
// padding: 16px;
// border-radius: 8px;
// border: 1px solid #DCDFEA;
// background: #FFF;
// // box-shadow: 0px 1px 3px 0px rgba(16, 24, 40, 0.10), 0px 1px 2px 0px rgba(16, 24, 40, 0.06);
// }
// &::-webkit-scrollbar {
// width: 0px;
// height: 0px;
// /* background-color: red; */
// }
// &::-webkit-scrollbar-thumb {
// border-radius: 15px;
// background-color: rgba(144, 147, 153, 0.3);
// }
// &::-webkit-scrollbar-thumb:hover {
// background-color: rgba(144, 147, 153, 0.5);
// }
// & {
// /* Firefox */
// scrollbar-width: none;
// /* auto, thin, none */
// scrollbar-color: rgba(144, 147, 153, 0.5);
// }
// }
}
.proxies {}
.custom-font {
font-family: Arial, sans-serif;
}
}

View File

@ -1,8 +1,9 @@
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'
import AntiDarkAnalysisNetwork from '@/pages/anti-dark-analysis-network'
import LazyLoader from '@/layout/LazyLoader'
import App from '@/App'
@ -15,7 +16,7 @@ export const router = createBrowserRouter([
children: [
{
index: true, // 默认路由
element: <Navigate to="/home" replace />, // 重定向到 /home
element: <Navigate to="/new-home" replace />, // 重定向到 /home
},
{
path: '/new-home',
@ -29,13 +30,17 @@ export const router = createBrowserRouter([
path: '/anti-forensics-forwarding',
element: <LazyLoader component={AntiForensicsForwardingPage} />,
},
{
path: '/anti-dark-analysis-network',
element: <LazyLoader component={AntiDarkAnalysisNetwork} />,
},
// {
// path: '/home',
// element: <LazyLoader component={HomePage} />,
// },
{
path: '/proxies',
element: <LazyLoader component={ProxiesPage} />,
element: <LazyLoader component={HomePage} />,
},
],
},

View File

@ -1,6 +1,7 @@
import { createSlice } from "@reduxjs/toolkit";
import { createSlice,createAsyncThunk } from "@reduxjs/toolkit";
import { v4 as uuid } from "uuid";
import { data1, data2 } from "./datas";
export interface Iweb3 {
id?: string;
name?: string;
@ -15,6 +16,10 @@ export interface Iweb3 {
numberTransactions?: string;
// 交易次数
transactions?: number | string;
height?: number | string;
txs: any[];
timerstamp?: string;
balanceToFixed?: string | number;
}
interface Iweb3Slice {
@ -23,11 +28,18 @@ interface Iweb3Slice {
newHomeProxies: any[];
path_list: any;
proxy_info: any;
clearWarningTimer: number | null;
clearFailTimer: number | null;
isLine: boolean;
maliciousNodeList: any[]; // 恶意节点
nodeDownList: any[]; // 节点下线
}
export const setProxiesList = createAsyncThunk(
'web3/setProxiesList',
async (payload, { dispatch }) => {
dispatch(setProxiesLine());
}
);
// 随机生成 0-100 之间的数字,保留一位小数或整数
const randomBalance = (): string => {
const value = Math.random() * 100;
@ -65,30 +77,14 @@ const initialState: Iweb3Slice = {
isLine: false,
web3List: [],
web3List2: [
{
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,
},
],
path_list: [
{
account: "admin",
account_is_admin: true,
exclusive: "none",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
proxies: [],
proxies_code: [],
use: true,
@ -98,14 +94,14 @@ const initialState: Iweb3Slice = {
proxy_info: {
exclusive: "",
name: "default(10.66.66.234)-c250",
name: "default(47.82.97.10)-c250",
wg: false,
change_time: 0,
change_at: 0,
proxies: [],
proxies: [
],
},
clearWarningTimer: null,
clearFailTimer: null,
newHomeProxies: [
{
authenticationPoint: [
@ -151,21 +147,59 @@ const initialState: Iweb3Slice = {
name: "newHomeProxies",
},
],
maliciousNodeList: [], // 恶意节点
nodeDownList: [], // 节点下线
};
export const appSlice = createSlice({
name: "web3",
initialState,
reducers: {
setProxiesList1: (state) => {
removeMaliciousNodeList: (state, action) => {
state.maliciousNodeList = state.maliciousNodeList.filter(
(item) => item.name !== action.payload.name
);
},
removeNodeDownList: (state, action) => {
state.nodeDownList = state.nodeDownList.filter(
(item) => item.name !== action.payload.name
);
},
setMaliciousNodeList: (state, action) => {
// 判断当前节点是否已经存在
const maliciousNode = state.maliciousNodeList.find(
(item) => action.payload.name === item.name
);
if (!maliciousNode) {
state.maliciousNodeList.push(action.payload);
}
},
setNodeDownList: (state, action) => {
// 判断当前节点是否已经存在
const nodeDown = state.nodeDownList.find(
(item) => action.payload.name === item.name
);
if (!nodeDown) {
state.nodeDownList.push(action.payload);
}
},
setProxiesList1: (state, action) => {
// state.proxy_info.prox
// 判断是否已经存在
const proxies = state.proxy_info.proxies.find(
(item: any) => item.name === "data1"
);
if (!proxies) {
state.proxy_info.proxies.push(data1);
const proxy_info = state.proxy_info;
if (proxy_info.proxies.length === 0) {
proxy_info.proxies.push({
name: "data1",
isLine: false,
data: action.payload,
});
} else {
proxy_info.proxies[0] = {
...proxy_info.proxies[0],
data: action.payload,
};
}
state.proxy_info = proxy_info;
},
setProxiesList2: (state) => {
// state.proxy_info.prox
@ -177,7 +211,7 @@ export const appSlice = createSlice({
state.proxy_info.proxies.push(data2);
}
},
setProxiesLine: (state) => {
setProxiesLine: (state,action) => {
if (state.proxy_info.proxies.length === 0) return;
// 判断一下如果state.proxy_info.proxies.lengt === 2并且 web3List.length ===1 那么 添加一个web3
// 如果state.proxy_info.proxies.lengt === 2并且 web3List.length ===2 那么不添加web3
@ -187,71 +221,31 @@ export const appSlice = createSlice({
// 进一步优化的代码
if (state.proxy_info.proxies.length === 0) return;
// 标记所有代理为在线 - 这个操作仍然需要
state.proxy_info.proxies = state.proxy_info.proxies.map((item: any) => {
item.isLine = true;
item.isLine = action.payload;
return item;
});
// 计算需要添加的钱包数量
const proxiesCount = state.proxy_info.proxies.length;
const currentWeb3Count = state.web3List.length;
// 检查是否需要添加钱包
if (
(proxiesCount === 2 && currentWeb3Count >= 2) ||
(proxiesCount === 1 && currentWeb3Count >= 1)
) {
// 已满足条件,不需要任何操作
return;
}
// 计算需要添加的钱包数量
let walletsToAdd = 0;
if (proxiesCount === 2) {
walletsToAdd = 2 - currentWeb3Count; // 最多添加到2个
} else if (proxiesCount === 1) {
walletsToAdd = 1 - currentWeb3Count; // 最多添加到1个
}
// 确保不会添加负数的钱包
walletsToAdd = Math.max(0, walletsToAdd);
// 只有在需要添加钱包时才创建新钱包
if (walletsToAdd > 0) {
const newWallets: Iweb3[] = [];
for (let i = 0; i < walletsToAdd; i++) {
const id = uuid();
newWallets.push({
id,
name: "Cardano Wallet",
payType: "ADA",
status: "active",
createdAt: 1737436420,
balance: randomBalance(),
upDatedAt: randomUpdatedAt(),
transactions: randomTransactionCount2(),
numberTransactions: randomTransactionCount(),
});
}
// 更新状态
state.web3List = [...state.web3List, ...newWallets];
console.log(state.web3List, "state.web3List");
}
},
setIsLine: (state, action) => {
state.isLine = action.payload;
},
setClearWarningTimer: (state, action) => {
state.clearWarningTimer = action.payload;
},
setClearFailTimer: (state, action) => {
state.clearFailTimer = action.payload;
},
setWeb3List: (state, action) => {
state.web3List = action.payload;
// 最多只存六条数据 action.payload 为item
const web3List = state.web3List;
// 判断如果源数据有六条或六条以上,那么截取最后五条数据
if (web3List.length >= 6) {
web3List.splice(0, 1);
}
// 判断一下当前的高度是否存在,如果存在那么就不添加
const isExist = web3List.find(
(item) => item.height === action.payload.height
);
if (!isExist) {
web3List.push(action.payload);
}
state.web3List = web3List;
},
setWeb3List2: (state, action) => {
state.web3List2 = action.payload;
@ -276,13 +270,15 @@ export const appSlice = createSlice({
});
export const {
removeMaliciousNodeList,
removeNodeDownList,
setMaliciousNodeList,
setNodeDownList,
setWeb3List,
setWeb3List2,
setProxyInfoProxies,
randomUpdateWeb3List,
randomUpdateWeb3List2,
setClearWarningTimer,
setClearFailTimer,
reset,
setIsLine,
setProxiesList1,

View File

@ -16,9 +16,15 @@ class FetchApi {
...config,
});
if (result.ok) {
return await result.json();
return {
success: true,
data: await result.json(),
};
} else {
return null;
return {
success: false,
data: await result.text(),
};
}
}

23
src/utils/eventBus.ts Normal file
View File

@ -0,0 +1,23 @@
import mitt from "mitt";
const eventBus = mitt();
// 事件类型
export const eventTypes = {
// 节点上线
NODE_UP: "node_up",
// 节点下线
NODE_DOWN: "node_down",
// 检测到恶意节点
MALICIOUS_NODE: "malicious_node",
// 节点预配置完成
NODE_INIT_COMPLATE: "node_init_complete",
// 节点清除
NODE_REMOVE: "node_remove",
// 添加节点
NODE_ADD: "node_add",
// 节点预配置
NODE_INIT: "node_init",
};
export default eventBus;

View File

@ -27,7 +27,7 @@ export class WebSocketClient {
}
// 发送消息
async sendMessage(message: string): Promise<void> {
async sendMessage(message: any): Promise<void> {
if (!this.ws) {
console.error('WebSocket is not connected')
return

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();
*/

10
src/vite-env.d.ts vendored
View File

@ -1,2 +1,12 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
// 定义你的环境变量,例如:
readonly VITE_BASE_URL: string
readonly VITE_BLOCK_URL: string
readonly VIET_EVENTS_WS_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}