Compare commits
37 Commits
main
...
torres/131
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d7d4cf3c | ||
|
|
8efc3f9b31 | ||
|
|
176aa823b1 | ||
|
|
4b2e53ac22 | ||
|
|
46a53d7b46 | ||
|
|
a785561aa9 | ||
|
|
e420fb0c1e | ||
|
|
a0e931c472 | ||
|
|
30a1a40a8c | ||
|
|
cbe3cb594e | ||
|
|
afbb74f823 | ||
|
|
f5d23a68c3 | ||
|
|
bdad7bc1da | ||
|
|
21d8ddabf1 | ||
|
|
91a7ca8c4b | ||
|
|
5151bd35d0 | ||
|
|
15bb6d8bc0 | ||
|
|
6357dfb24c | ||
|
|
a9d30c05c8 | ||
|
|
bab441979f | ||
|
|
1928b112fb | ||
|
|
32f38e1efb | ||
|
|
2efe64862d | ||
|
|
e80e4035a0 | ||
|
|
c753617ee9 | ||
|
|
a86b20f9d5 | ||
|
|
98a4e9b388 | ||
|
|
bd6f658c73 | ||
|
|
fae2e8e7ec | ||
|
|
5000c88f0f | ||
|
|
1693316766 | ||
|
|
d220b7b0f5 | ||
|
|
ffabc1ccbe | ||
|
|
9f09e8ba61 | ||
|
|
064973aaf0 | ||
|
|
41dba8cb7c | ||
|
|
652aea1829 |
14
.env
14
.env
@ -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
12
.env copy
Normal 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
|
||||
@ -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
109
pnpm-lock.yaml
generated
@ -62,6 +62,9 @@ importers:
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ~2
|
||||
version: 2.2.0
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ~2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-global-shortcut':
|
||||
specifier: ~2
|
||||
version: 2.2.0
|
||||
@ -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: {}
|
||||
|
||||
1
src-tauri/.gitignore
vendored
1
src-tauri/.gitignore
vendored
@ -8,6 +8,5 @@
|
||||
|
||||
.vscode/*
|
||||
|
||||
sidecar/*
|
||||
|
||||
system-service/.vscode/*
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -2960,6 +2960,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-os",
|
||||
|
||||
@ -69,6 +69,7 @@ tauri-plugin-process = "2"
|
||||
tauri-plugin-http = "2"
|
||||
tauri-plugin-store = "2"
|
||||
tauri-plugin-websocket = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
@ -55,6 +57,16 @@
|
||||
},
|
||||
"store:default",
|
||||
"websocket:default",
|
||||
"http:default"
|
||||
"http:default",
|
||||
"fs:default",
|
||||
{
|
||||
"identifier": "fs:allow-exists",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$APPDATA/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fs:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
BIN
src-tauri/sidecar/paw-core-aarch64-apple-darwin
Normal file
BIN
src-tauri/sidecar/paw-core-aarch64-apple-darwin
Normal file
Binary file not shown.
BIN
src-tauri/sidecar/paw-core-x86_64-pc-windows-msvc.exe
Normal file
BIN
src-tauri/sidecar/paw-core-x86_64-pc-windows-msvc.exe
Normal file
Binary file not shown.
BIN
src-tauri/sidecar/paw-install-service-x86_64-pc-windows-msvc.exe
Normal file
BIN
src-tauri/sidecar/paw-install-service-x86_64-pc-windows-msvc.exe
Normal file
Binary file not shown.
BIN
src-tauri/sidecar/paw-system-service-x86_64-pc-windows-msvc.exe
Normal file
BIN
src-tauri/sidecar/paw-system-service-x86_64-pc-windows-msvc.exe
Normal file
Binary file not shown.
Binary file not shown.
@ -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]
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
303
src/App.tsx
303
src/App.tsx
@ -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>
|
||||
|
||||
@ -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
27
src/api/flying-line.ts
Normal 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;
|
||||
};
|
||||
BIN
src/assets/image/home/close-proxy.png
Normal file
BIN
src/assets/image/home/close-proxy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
5
src/assets/svg/layout/anti-dark-analysis-network.svg
Normal file
5
src/assets/svg/layout/anti-dark-analysis-network.svg
Normal 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 / 打击节点">
|
||||
<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 |
@ -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 }
|
||||
/**
|
||||
* 节点信息
|
||||
*/
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
1485
src/data/index.ts
1485
src/data/index.ts
File diff suppressed because it is too large
Load Diff
26
src/hooks/useEventBus.ts
Normal file
26
src/hooks/useEventBus.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}, []);
|
||||
}
|
||||
|
||||
199
src/index.scss
199
src/index.scss
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
1248
src/pages/anti-dark-analysis-network/components/world-geo.tsx
Normal file
1248
src/pages/anti-dark-analysis-network/components/world-geo.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1175
src/pages/anti-dark-analysis-network/data/index.ts
Normal file
1175
src/pages/anti-dark-analysis-network/data/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
449
src/pages/anti-dark-analysis-network/data/mockData.ts
Normal file
449
src/pages/anti-dark-analysis-network/data/mockData.ts
Normal 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,
|
||||
}
|
||||
0
src/pages/anti-dark-analysis-network/index.scss
Normal file
0
src/pages/anti-dark-analysis-network/index.scss
Normal file
310
src/pages/anti-dark-analysis-network/index.tsx
Normal file
310
src/pages/anti-dark-analysis-network/index.tsx
Normal 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;
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -1,224 +0,0 @@
|
||||
import { Form, FormInstance } from "antd";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FormDialog } from "@/components/FormDialog";
|
||||
import { countryCodeMap } from "@/data";
|
||||
import { DIALOGTYPE } from "../../index";
|
||||
|
||||
import "./index.scss";
|
||||
import { DefaultLink } from "./pathChoose";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export interface DialogConfig {
|
||||
title: string;
|
||||
desc: string;
|
||||
successText: string;
|
||||
}
|
||||
|
||||
const PledgeAmount = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value?: string;
|
||||
onChange?: (data: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
|
||||
placeholder="请输质押金额*"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange?.(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<span>SOL</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const IpPortInput = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value?: { port: string; ip: string };
|
||||
onChange?: (data: { port: string; ip: string }) => void;
|
||||
}) => {
|
||||
const [ipAndPort, setIpAndPort] = useState<{
|
||||
port: string;
|
||||
ip: string;
|
||||
}>(value ? value : { port: "", ip: "" });
|
||||
const handleChagne = ({
|
||||
key,
|
||||
val,
|
||||
}: {
|
||||
key: "port" | "ip";
|
||||
val: string;
|
||||
}) => {
|
||||
const newValue = value ? { ...value } : { port: "", ip: "" };
|
||||
newValue[key] = val;
|
||||
onChange?.(newValue);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setIpAndPort(value);
|
||||
}
|
||||
}, [value]);
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
|
||||
placeholder="请输入IP*"
|
||||
value={ipAndPort?.ip}
|
||||
onChange={(e) => {
|
||||
handleChagne?.({ key: "ip", val: e.target.value });
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
className="data-[state=checked]:bg-[#1E3A8A] "
|
||||
placeholder="请输入端口"
|
||||
type="number"
|
||||
min={0}
|
||||
max={65535}
|
||||
value={ipAndPort?.port}
|
||||
onChange={(e) => {
|
||||
handleChagne?.({ key: "port", val: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeForm = ({ form }: { form: FormInstance }) => {
|
||||
return (
|
||||
<Form
|
||||
className="-mt-1"
|
||||
form={form}
|
||||
name="dynamic_form_nest_item"
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
>
|
||||
{/* <Form.Item name="uid" className="hidden">
|
||||
<div className="hidden">uid</div>
|
||||
</Form.Item> */}
|
||||
<Form.Item name="nodePublicKey" label="节点身份公钥">
|
||||
<Input
|
||||
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
|
||||
placeholder="节点身份公钥*"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="nodeMetadata" label="节点元数据">
|
||||
<Input
|
||||
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
|
||||
placeholder="请输入TLS Pubkey*"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="ipAndPort" label="IP+端口">
|
||||
<IpPortInput />
|
||||
</Form.Item>
|
||||
<Form.Item name="pledgeAmount" label="质押金额">
|
||||
<PledgeAmount />
|
||||
</Form.Item>
|
||||
<Form.Item name="walletAddress" label="钱包地址">
|
||||
<Input
|
||||
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
|
||||
placeholder="钱包地址*"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkForm = ({ form }: { form: FormInstance }) => {
|
||||
return (
|
||||
<Form
|
||||
className="-mt-1"
|
||||
form={form}
|
||||
name="dynamic_form_nest_item"
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="inbound"
|
||||
label="入口节点"
|
||||
rules={[{ required: true, message: "请选择入口节点" }]}
|
||||
>
|
||||
<DefaultLink
|
||||
des="入口节点"
|
||||
type="entry"
|
||||
countries={Object.keys(countryCodeMap)
|
||||
// .splice(0, 50)
|
||||
.map((key) => key)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="outbound"
|
||||
label="出口节点"
|
||||
rules={[{ required: true, message: "请选择出口节点" }]}
|
||||
>
|
||||
<DefaultLink
|
||||
des="出口节点"
|
||||
type="exit"
|
||||
countries={Object.keys(countryCodeMap)
|
||||
// .splice(0, 50)
|
||||
.map((key) => key)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormAlertDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
successHandle,
|
||||
dialogLoading,
|
||||
form,
|
||||
type,
|
||||
canSubmit,
|
||||
handleSelectFile,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
successHandle: () => void;
|
||||
dialogLoading: boolean;
|
||||
canSubmit: boolean;
|
||||
form: FormInstance;
|
||||
type: DialogConfig;
|
||||
handleSelectFile: () => void;
|
||||
}) => {
|
||||
const showDialog = (open: boolean) => {
|
||||
setOpen(open);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
open={open}
|
||||
openChange={showDialog}
|
||||
title={type.title}
|
||||
describe={type.desc}
|
||||
successText={type.successText}
|
||||
successHandle={successHandle}
|
||||
submitLoading={dialogLoading}
|
||||
form={form}
|
||||
contentClass="w-[850px] flex flex-col max-h-[calc(100vh-100px)] overflow-y-hidden "
|
||||
successStyle={
|
||||
canSubmit
|
||||
? "bg-[#1E3A8A] hover:bg-[#1D4ED8] active:bg-[#1E40AF]"
|
||||
: "bg-[#1E3A8A] hover:bg-[#1E3A8A] opacity-50"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="absolute top-3 right-12 bg-transparent text-zinc-900 border border-zinc-200"
|
||||
onClick={() => handleSelectFile()}
|
||||
>
|
||||
批量导入
|
||||
</Button>
|
||||
{open && type.title === DIALOGTYPE.ADDNode.title ? (
|
||||
<NodeForm form={form} />
|
||||
) : (
|
||||
<NetworkForm form={form} />
|
||||
)}
|
||||
</FormDialog>
|
||||
);
|
||||
};
|
||||
@ -1,258 +0,0 @@
|
||||
import { useMemo, useState, useEffect } from 'react'
|
||||
// import LinkButtonCol from "@/assets/svg/LinkButtonCol.svg?react";
|
||||
import Gurad from '@/assets/svg/ProxiesGuard.svg?react'
|
||||
import Exit from '@/assets/svg/Exit.svg?react'
|
||||
import Down from '@/assets/svg/down.svg?react'
|
||||
import HiddenNode from '@/assets/svg/HiddenNode.svg?react'
|
||||
import InletNodeSvg from '@/assets/svg/InletNode.svg?react'
|
||||
// import EntryNode from "@/assets/svg/EntryNode.svg?react";
|
||||
|
||||
import { cn, getUrl } from '@/lib/utils'
|
||||
// import { useProxiesExit, useProxiesGuard } from "@/api/link";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
import { Combobox, IComboboxValue } from '@/components/Combobox'
|
||||
// import { ProxiesList } from "@/pages/home/types";
|
||||
import { SearchOptionText } from '@/pages/home/components/MultipleLinks'
|
||||
import { countryCodeMap } from '@/pages/home/data'
|
||||
|
||||
export const DefaultLink = ({
|
||||
value,
|
||||
flag = '',
|
||||
className,
|
||||
des,
|
||||
type = 'hidden',
|
||||
disabled,
|
||||
countries,
|
||||
isChecked = true,
|
||||
onChange,
|
||||
}: {
|
||||
value?: string
|
||||
des: string
|
||||
flag?: string
|
||||
countries: any[]
|
||||
className?: string
|
||||
type?: string
|
||||
disabled?: boolean
|
||||
isChecked?: boolean
|
||||
onChange?: (value: IComboboxValue) => void
|
||||
}) => {
|
||||
// const ingressName = countryCodeMap[flag.toUpperCase()]
|
||||
|
||||
const [ingressName, setIngressName] = useState(des ?? '')
|
||||
const [ingressOptions, setIngressOptions] = useState<
|
||||
{
|
||||
label: string
|
||||
value: string
|
||||
icon: JSX.Element
|
||||
num: number
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const getOptions = () => {
|
||||
const options = countries.map((item) => {
|
||||
const name = countryCodeMap[item.toUpperCase()]
|
||||
return {
|
||||
label: name,
|
||||
value: item,
|
||||
icon: (
|
||||
<div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] shrink-0">
|
||||
<img
|
||||
src={getUrl(`image/res/flag/${item.toUpperCase()}.png`)}
|
||||
alt=""
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
num: 0,
|
||||
}
|
||||
})
|
||||
setIngressOptions(options)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// type === 'hidden' &&
|
||||
isChecked && setIngressName(countryCodeMap[flag.toUpperCase()] ?? des)
|
||||
getOptions()
|
||||
}, [flag])
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setIngressName(value)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center renderButtonStyle', className)}>
|
||||
<Combobox
|
||||
value={ingressName}
|
||||
typeBorder
|
||||
onChange={(e) => {
|
||||
onChange?.(e)
|
||||
}}
|
||||
title="输入国家名称"
|
||||
radioText={SearchOptionText}
|
||||
downIcon={<Down />}
|
||||
options={ingressOptions}
|
||||
placeholder={
|
||||
type === 'entry'
|
||||
? '入口节点'
|
||||
: type === 'hidden'
|
||||
? '隐匿节点'
|
||||
: '出口节点'
|
||||
}
|
||||
muiltip={false}
|
||||
contentClass="w-full p-2 pt-0 linkAdd_ComboxContent my-[4px]"
|
||||
contentProps={{
|
||||
alignOffset: 0,
|
||||
}}
|
||||
disabled={disabled}
|
||||
comboxTitle={<ComboxTitle type={type} />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ComboxTitle = ({ type }: { type: string }) => {
|
||||
const getNode = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'guard':
|
||||
return (
|
||||
<>
|
||||
<Gurad className="mr-2 w-6 h-6" fill="#059669" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
守卫节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
case 'hidden':
|
||||
return (
|
||||
<>
|
||||
<HiddenNode className="mr-2 w-6 h-6" fill="#059669" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
隐匿节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
case 'entry':
|
||||
return (
|
||||
<>
|
||||
<InletNodeSvg className="mr-2 w-6 h-6" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
入口节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Exit className="mr-2" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
出口节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [type])
|
||||
|
||||
return <div className="flex items-center mb-1 mt-2 p-1">{getNode}</div>
|
||||
}
|
||||
// export const PathChoose = ({ value, onChange, pathText, type }:
|
||||
// {
|
||||
// value?: string
|
||||
// type: string
|
||||
// onChange?: (value: IComboboxValue) => void
|
||||
// pathText: {
|
||||
// guard: string
|
||||
// exit: string
|
||||
// }
|
||||
// }
|
||||
// ) => {
|
||||
// const [flagText, setFlagText] = useState<ProxiesList>()
|
||||
// const { data: proxiesList, refetch: refetchGuard } = useProxiesGuard()
|
||||
// const { data: proxiesExit, refetch: refetchExit } = useProxiesExit()
|
||||
// const [maxHeightClass, setMaxHeightClass] = useState("max-h-[300px]");
|
||||
|
||||
// const currentList = useMemo(() => {
|
||||
// return type === 'guard' ? proxiesList?.data : proxiesExit?.data
|
||||
// }, [proxiesList, proxiesExit, type])
|
||||
|
||||
// const getOptions = (value_: ProxiesList[]) => {
|
||||
// const options = value_
|
||||
// .filter(item => item.name === value)
|
||||
// .map(item => {
|
||||
// return {
|
||||
// label: item.name,
|
||||
// value: item.name,
|
||||
// icon: <div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] mr-2 shrink-0">
|
||||
// {/* <img src={getUrl(`image/res/flag/${item.country_code.toUpperCase()}.png`)} alt="" className="w-4 h-4" /> */}
|
||||
// </div>,
|
||||
// num: item.delay,
|
||||
// }
|
||||
// })
|
||||
// return options
|
||||
// }
|
||||
|
||||
// const onHandleChange = (e: IComboboxValue) => {
|
||||
// const value_ = currentList?.find((index: any) => index.name === e)
|
||||
// setFlagText(value_)
|
||||
// onChange?.(e)
|
||||
// }
|
||||
|
||||
// const currentOption = useMemo(() => {
|
||||
// if (!currentList) return []
|
||||
// const value_ = currentList.find((index: any) => index.name === value)
|
||||
// setFlagText(value_)
|
||||
// return getOptions(currentList)
|
||||
// }, [currentList])
|
||||
|
||||
// useEffect(() => {
|
||||
// const updateMaxHeight = () => {
|
||||
// if (window.innerHeight < 1080) {
|
||||
// setMaxHeightClass("max-h-[260px]");
|
||||
// } else {
|
||||
// setMaxHeightClass("max-h-[300px]");
|
||||
// }
|
||||
// };
|
||||
|
||||
// updateMaxHeight(); // 初始调用一次
|
||||
// window.addEventListener("resize", updateMaxHeight); // 监听窗口大小变化
|
||||
// return () => window.removeEventListener("resize", updateMaxHeight); // 清除监听器
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <LinkButtonCol className="linkbutton" />
|
||||
// <div className="flex justify-between items-center mt-3">
|
||||
// <div className="flex-1 mr-[10px]">
|
||||
// <div className="mb-2 font-medium">{pathText.guard}</div>
|
||||
// <DefaultLink
|
||||
// des={flagText?.ingress_country_name ?? "隐匿节点"}
|
||||
// flag={flagText?.ingress_country_code}
|
||||
// type='hidden'
|
||||
// disabled={!value} />
|
||||
// </div>
|
||||
// <div className="flex-1">
|
||||
// <div className="mb-2 font-medium">{pathText.exit}</div>
|
||||
// <Combobox
|
||||
// value={value}
|
||||
// onChange={onHandleChange}
|
||||
// typeBorder
|
||||
// radioText={SearchOptionText}
|
||||
// downIcon={<Down />}
|
||||
// options={currentOption}
|
||||
// onComboxOpen={type === 'guard' ? refetchGuard : refetchExit}
|
||||
// placeholder='请选择节点'
|
||||
// muiltip={false}
|
||||
// contentClass="p-2 pt-0 linkAdd_ComboxContent my-[4px]"
|
||||
// commandListClassName={maxHeightClass}
|
||||
// ellipsisTooltipClassName="text-[16px]"
|
||||
// comboxTitle={<ComboxTitle type={type} />}
|
||||
// contentProps={{
|
||||
// 'alignOffset': -380
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
@ -499,7 +499,6 @@ export const WorldGeo = memo(
|
||||
label: {
|
||||
show: false,
|
||||
formatter: (params: any) => {
|
||||
console.log(params, "params");
|
||||
return `{${params.data.datas.country_code}|}`;
|
||||
},
|
||||
},
|
||||
|
||||
1595
src/pages/anti-forensics-forwarding/components/world-geo copy 3.tsx
Normal file
1595
src/pages/anti-forensics-forwarding/components/world-geo copy 3.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
// }
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
.linkAdd_ComboxContent{
|
||||
width: 800px !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@ -1,224 +0,0 @@
|
||||
import { Form, FormInstance } from "antd";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FormDialog } from "@/components/FormDialog";
|
||||
import { countryCodeMap } from "@/data";
|
||||
import { DIALOGTYPE } from "../../index";
|
||||
|
||||
import "./index.scss";
|
||||
import { DefaultLink } from "./pathChoose";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export interface DialogConfig {
|
||||
title: string;
|
||||
desc: string;
|
||||
successText: string;
|
||||
}
|
||||
|
||||
const PledgeAmount = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value?: string;
|
||||
onChange?: (data: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
|
||||
placeholder="请输质押金额*"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange?.(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<span>SOL</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const IpPortInput = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value?: { port: string; ip: string };
|
||||
onChange?: (data: { port: string; ip: string }) => void;
|
||||
}) => {
|
||||
const [ipAndPort, setIpAndPort] = useState<{
|
||||
port: string;
|
||||
ip: string;
|
||||
}>(value ? value : { port: "", ip: "" });
|
||||
const handleChagne = ({
|
||||
key,
|
||||
val,
|
||||
}: {
|
||||
key: "port" | "ip";
|
||||
val: string;
|
||||
}) => {
|
||||
const newValue = value ? { ...value } : { port: "", ip: "" };
|
||||
newValue[key] = val;
|
||||
onChange?.(newValue);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setIpAndPort(value);
|
||||
}
|
||||
}, [value]);
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
className="data-[state=checked]:bg-[#1E3A8A] flex-shrink-0 !w-[600px]"
|
||||
placeholder="请输入IP*"
|
||||
value={ipAndPort?.ip}
|
||||
onChange={(e) => {
|
||||
handleChagne?.({ key: "ip", val: e.target.value });
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
className="data-[state=checked]:bg-[#1E3A8A] "
|
||||
placeholder="请输入端口"
|
||||
type="number"
|
||||
min={0}
|
||||
max={65535}
|
||||
value={ipAndPort?.port}
|
||||
onChange={(e) => {
|
||||
handleChagne?.({ key: "port", val: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeForm = ({ form }: { form: FormInstance }) => {
|
||||
return (
|
||||
<Form
|
||||
className="-mt-1"
|
||||
form={form}
|
||||
name="dynamic_form_nest_item"
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
>
|
||||
{/* <Form.Item name="uid" className="hidden">
|
||||
<div className="hidden">uid</div>
|
||||
</Form.Item> */}
|
||||
<Form.Item name="nodePublicKey" label="节点身份公钥">
|
||||
<Input
|
||||
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
|
||||
placeholder="节点身份公钥*"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="nodeMetadata" label="节点元数据">
|
||||
<Input
|
||||
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
|
||||
placeholder="请输入TLS Pubkey*"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="ipAndPort" label="IP+端口">
|
||||
<IpPortInput />
|
||||
</Form.Item>
|
||||
<Form.Item name="pledgeAmount" label="质押金额">
|
||||
<PledgeAmount />
|
||||
</Form.Item>
|
||||
<Form.Item name="walletAddress" label="钱包地址">
|
||||
<Input
|
||||
className="link_name_input placeholder:text-base placeholder:text-zinc-400 text-[16px]"
|
||||
placeholder="钱包地址*"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkForm = ({ form }: { form: FormInstance }) => {
|
||||
return (
|
||||
<Form
|
||||
className="-mt-1"
|
||||
form={form}
|
||||
name="dynamic_form_nest_item"
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="inbound"
|
||||
label="入口节点"
|
||||
rules={[{ required: true, message: "请选择入口节点" }]}
|
||||
>
|
||||
<DefaultLink
|
||||
des="入口节点"
|
||||
type="entry"
|
||||
countries={Object.keys(countryCodeMap)
|
||||
// .splice(0, 50)
|
||||
.map((key) => key)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="outbound"
|
||||
label="出口节点"
|
||||
rules={[{ required: true, message: "请选择出口节点" }]}
|
||||
>
|
||||
<DefaultLink
|
||||
des="出口节点"
|
||||
type="exit"
|
||||
countries={Object.keys(countryCodeMap)
|
||||
// .splice(0, 50)
|
||||
.map((key) => key)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormAlertDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
successHandle,
|
||||
dialogLoading,
|
||||
form,
|
||||
type,
|
||||
canSubmit,
|
||||
handleSelectFile,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
successHandle: () => void;
|
||||
dialogLoading: boolean;
|
||||
canSubmit: boolean;
|
||||
form: FormInstance;
|
||||
type: DialogConfig;
|
||||
handleSelectFile: () => void;
|
||||
}) => {
|
||||
const showDialog = (open: boolean) => {
|
||||
setOpen(open);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
open={open}
|
||||
openChange={showDialog}
|
||||
title={type.title}
|
||||
describe={type.desc}
|
||||
successText={type.successText}
|
||||
successHandle={successHandle}
|
||||
submitLoading={dialogLoading}
|
||||
form={form}
|
||||
contentClass="w-[850px] flex flex-col max-h-[calc(100vh-100px)] overflow-y-hidden "
|
||||
successStyle={
|
||||
canSubmit
|
||||
? "bg-[#1E3A8A] hover:bg-[#1D4ED8] active:bg-[#1E40AF]"
|
||||
: "bg-[#1E3A8A] hover:bg-[#1E3A8A] opacity-50"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="absolute top-3 right-12 bg-transparent text-zinc-900 border border-zinc-200"
|
||||
onClick={() => handleSelectFile()}
|
||||
>
|
||||
批量导入
|
||||
</Button>
|
||||
{open && type.title === DIALOGTYPE.ADDNode.title ? (
|
||||
<NodeForm form={form} />
|
||||
) : (
|
||||
<NetworkForm form={form} />
|
||||
)}
|
||||
</FormDialog>
|
||||
);
|
||||
};
|
||||
@ -1,258 +0,0 @@
|
||||
import { useMemo, useState, useEffect } from 'react'
|
||||
// import LinkButtonCol from "@/assets/svg/LinkButtonCol.svg?react";
|
||||
import Gurad from '@/assets/svg/ProxiesGuard.svg?react'
|
||||
import Exit from '@/assets/svg/Exit.svg?react'
|
||||
import Down from '@/assets/svg/down.svg?react'
|
||||
import HiddenNode from '@/assets/svg/HiddenNode.svg?react'
|
||||
import InletNodeSvg from '@/assets/svg/InletNode.svg?react'
|
||||
// import EntryNode from "@/assets/svg/EntryNode.svg?react";
|
||||
|
||||
import { cn, getUrl } from '@/lib/utils'
|
||||
// import { useProxiesExit, useProxiesGuard } from "@/api/link";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
import { Combobox, IComboboxValue } from '@/components/Combobox'
|
||||
// import { ProxiesList } from "@/pages/home/types";
|
||||
import { SearchOptionText } from '@/pages/home/components/MultipleLinks'
|
||||
import { countryCodeMap } from '@/pages/home/data'
|
||||
|
||||
export const DefaultLink = ({
|
||||
value,
|
||||
flag = '',
|
||||
className,
|
||||
des,
|
||||
type = 'hidden',
|
||||
disabled,
|
||||
countries,
|
||||
isChecked = true,
|
||||
onChange,
|
||||
}: {
|
||||
value?: string
|
||||
des: string
|
||||
flag?: string
|
||||
countries: any[]
|
||||
className?: string
|
||||
type?: string
|
||||
disabled?: boolean
|
||||
isChecked?: boolean
|
||||
onChange?: (value: IComboboxValue) => void
|
||||
}) => {
|
||||
// const ingressName = countryCodeMap[flag.toUpperCase()]
|
||||
|
||||
const [ingressName, setIngressName] = useState(des ?? '')
|
||||
const [ingressOptions, setIngressOptions] = useState<
|
||||
{
|
||||
label: string
|
||||
value: string
|
||||
icon: JSX.Element
|
||||
num: number
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const getOptions = () => {
|
||||
const options = countries.map((item) => {
|
||||
const name = countryCodeMap[item.toUpperCase()]
|
||||
return {
|
||||
label: name,
|
||||
value: item,
|
||||
icon: (
|
||||
<div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] shrink-0">
|
||||
<img
|
||||
src={getUrl(`image/res/flag/${item.toUpperCase()}.png`)}
|
||||
alt=""
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
num: 0,
|
||||
}
|
||||
})
|
||||
setIngressOptions(options)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// type === 'hidden' &&
|
||||
isChecked && setIngressName(countryCodeMap[flag.toUpperCase()] ?? des)
|
||||
getOptions()
|
||||
}, [flag])
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setIngressName(value)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center renderButtonStyle', className)}>
|
||||
<Combobox
|
||||
value={ingressName}
|
||||
typeBorder
|
||||
onChange={(e) => {
|
||||
onChange?.(e)
|
||||
}}
|
||||
title="输入国家名称"
|
||||
radioText={SearchOptionText}
|
||||
downIcon={<Down />}
|
||||
options={ingressOptions}
|
||||
placeholder={
|
||||
type === 'entry'
|
||||
? '入口节点'
|
||||
: type === 'hidden'
|
||||
? '隐匿节点'
|
||||
: '出口节点'
|
||||
}
|
||||
muiltip={false}
|
||||
contentClass="w-full p-2 pt-0 linkAdd_ComboxContent my-[4px]"
|
||||
contentProps={{
|
||||
alignOffset: 0,
|
||||
}}
|
||||
disabled={disabled}
|
||||
comboxTitle={<ComboxTitle type={type} />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ComboxTitle = ({ type }: { type: string }) => {
|
||||
const getNode = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'guard':
|
||||
return (
|
||||
<>
|
||||
<Gurad className="mr-2 w-6 h-6" fill="#059669" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
守卫节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
case 'hidden':
|
||||
return (
|
||||
<>
|
||||
<HiddenNode className="mr-2 w-6 h-6" fill="#059669" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
隐匿节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
case 'entry':
|
||||
return (
|
||||
<>
|
||||
<InletNodeSvg className="mr-2 w-6 h-6" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
入口节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Exit className="mr-2" />
|
||||
<span className="text-[#18181B] text-base font-semibold">
|
||||
出口节点
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [type])
|
||||
|
||||
return <div className="flex items-center mb-1 mt-2 p-1">{getNode}</div>
|
||||
}
|
||||
// export const PathChoose = ({ value, onChange, pathText, type }:
|
||||
// {
|
||||
// value?: string
|
||||
// type: string
|
||||
// onChange?: (value: IComboboxValue) => void
|
||||
// pathText: {
|
||||
// guard: string
|
||||
// exit: string
|
||||
// }
|
||||
// }
|
||||
// ) => {
|
||||
// const [flagText, setFlagText] = useState<ProxiesList>()
|
||||
// const { data: proxiesList, refetch: refetchGuard } = useProxiesGuard()
|
||||
// const { data: proxiesExit, refetch: refetchExit } = useProxiesExit()
|
||||
// const [maxHeightClass, setMaxHeightClass] = useState("max-h-[300px]");
|
||||
|
||||
// const currentList = useMemo(() => {
|
||||
// return type === 'guard' ? proxiesList?.data : proxiesExit?.data
|
||||
// }, [proxiesList, proxiesExit, type])
|
||||
|
||||
// const getOptions = (value_: ProxiesList[]) => {
|
||||
// const options = value_
|
||||
// .filter(item => item.name === value)
|
||||
// .map(item => {
|
||||
// return {
|
||||
// label: item.name,
|
||||
// value: item.name,
|
||||
// icon: <div className="w-[16px] h-[16px] flex items-center justify-center rounded-full bg-[#FFF] mr-2 shrink-0">
|
||||
// {/* <img src={getUrl(`image/res/flag/${item.country_code.toUpperCase()}.png`)} alt="" className="w-4 h-4" /> */}
|
||||
// </div>,
|
||||
// num: item.delay,
|
||||
// }
|
||||
// })
|
||||
// return options
|
||||
// }
|
||||
|
||||
// const onHandleChange = (e: IComboboxValue) => {
|
||||
// const value_ = currentList?.find((index: any) => index.name === e)
|
||||
// setFlagText(value_)
|
||||
// onChange?.(e)
|
||||
// }
|
||||
|
||||
// const currentOption = useMemo(() => {
|
||||
// if (!currentList) return []
|
||||
// const value_ = currentList.find((index: any) => index.name === value)
|
||||
// setFlagText(value_)
|
||||
// return getOptions(currentList)
|
||||
// }, [currentList])
|
||||
|
||||
// useEffect(() => {
|
||||
// const updateMaxHeight = () => {
|
||||
// if (window.innerHeight < 1080) {
|
||||
// setMaxHeightClass("max-h-[260px]");
|
||||
// } else {
|
||||
// setMaxHeightClass("max-h-[300px]");
|
||||
// }
|
||||
// };
|
||||
|
||||
// updateMaxHeight(); // 初始调用一次
|
||||
// window.addEventListener("resize", updateMaxHeight); // 监听窗口大小变化
|
||||
// return () => window.removeEventListener("resize", updateMaxHeight); // 清除监听器
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <LinkButtonCol className="linkbutton" />
|
||||
// <div className="flex justify-between items-center mt-3">
|
||||
// <div className="flex-1 mr-[10px]">
|
||||
// <div className="mb-2 font-medium">{pathText.guard}</div>
|
||||
// <DefaultLink
|
||||
// des={flagText?.ingress_country_name ?? "隐匿节点"}
|
||||
// flag={flagText?.ingress_country_code}
|
||||
// type='hidden'
|
||||
// disabled={!value} />
|
||||
// </div>
|
||||
// <div className="flex-1">
|
||||
// <div className="mb-2 font-medium">{pathText.exit}</div>
|
||||
// <Combobox
|
||||
// value={value}
|
||||
// onChange={onHandleChange}
|
||||
// typeBorder
|
||||
// radioText={SearchOptionText}
|
||||
// downIcon={<Down />}
|
||||
// options={currentOption}
|
||||
// onComboxOpen={type === 'guard' ? refetchGuard : refetchExit}
|
||||
// placeholder='请选择节点'
|
||||
// muiltip={false}
|
||||
// contentClass="p-2 pt-0 linkAdd_ComboxContent my-[4px]"
|
||||
// commandListClassName={maxHeightClass}
|
||||
// ellipsisTooltipClassName="text-[16px]"
|
||||
// comboxTitle={<ComboxTitle type={type} />}
|
||||
// contentProps={{
|
||||
// 'alignOffset': -380
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
||||
@ -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;
|
||||
// }
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
23
src/utils/eventBus.ts
Normal 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;
|
||||
@ -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
|
||||
|
||||
245
src/utils/webSocketClientV2.ts
Normal file
245
src/utils/webSocketClientV2.ts
Normal file
@ -0,0 +1,245 @@
|
||||
import WebSocket, {
|
||||
Message,
|
||||
ConnectionConfig,
|
||||
} from '@tauri-apps/plugin-websocket'
|
||||
|
||||
export class WebSocketClient {
|
||||
private ws: WebSocket | null = null
|
||||
private isConnected = false
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 5
|
||||
private reconnectDelay = 1000 // 初始重连延迟(毫秒)
|
||||
private listeners: ((msg: Message) => void)[] = []
|
||||
private autoReconnect = true
|
||||
|
||||
// 构造函数初始化 WebSocket 连接
|
||||
constructor(
|
||||
private url: string,
|
||||
private config: ConnectionConfig = {},
|
||||
options?: {
|
||||
autoReconnect?: boolean,
|
||||
maxReconnectAttempts?: number,
|
||||
reconnectDelay?: number
|
||||
}
|
||||
) {
|
||||
if (options) {
|
||||
this.autoReconnect = options.autoReconnect ?? true
|
||||
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5
|
||||
this.reconnectDelay = options.reconnectDelay ?? 1000
|
||||
}
|
||||
}
|
||||
|
||||
// 连接到 WebSocket
|
||||
async connect(): Promise<boolean> {
|
||||
try {
|
||||
if (this.isConnected && this.ws) {
|
||||
console.log('Already connected')
|
||||
return true
|
||||
}
|
||||
|
||||
this.ws = await WebSocket.connect(this.url, this.config)
|
||||
this.isConnected = true
|
||||
this.reconnectAttempts = 0 // 重置重连计数
|
||||
console.log(`Connected to ${this.url}`)
|
||||
|
||||
// 重新添加之前的所有监听器
|
||||
for (const listener of this.listeners) {
|
||||
this.ws.addListener(listener)
|
||||
}
|
||||
|
||||
// 添加内部监听器来处理连接关闭
|
||||
this.ws.addListener(this.handleWebSocketMessage.bind(this))
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to connect:', error)
|
||||
this.isConnected = false
|
||||
this.ws = null
|
||||
|
||||
// 如果启用了自动重连,尝试重连
|
||||
if (this.autoReconnect) {
|
||||
return this.attemptReconnect()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 WebSocket 消息,特别是关闭事件
|
||||
private async handleWebSocketMessage(msg: Message): Promise<void> {
|
||||
if (msg.type === 'Close') {
|
||||
this.isConnected = false
|
||||
this.ws = null
|
||||
console.log('Connection closed by server')
|
||||
|
||||
// 如果启用了自动重连,尝试重连
|
||||
if (this.autoReconnect) {
|
||||
await this.attemptReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试重新连接
|
||||
private async attemptReconnect(): Promise<boolean> {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error(`Failed to reconnect after ${this.maxReconnectAttempts} attempts`)
|
||||
return false
|
||||
}
|
||||
|
||||
this.reconnectAttempts++
|
||||
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1) // 指数退避策略
|
||||
|
||||
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms...`)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
|
||||
try {
|
||||
return await this.connect()
|
||||
} catch (error) {
|
||||
console.error('Reconnection attempt failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 手动重连
|
||||
async reconnect(): Promise<boolean> {
|
||||
this.reconnectAttempts = 0 // 重置重连计数
|
||||
if (this.isConnected && this.ws) {
|
||||
try {
|
||||
await this.disconnect()
|
||||
} catch (error) {
|
||||
console.error('Error during disconnect before reconnect:', error)
|
||||
}
|
||||
}
|
||||
return this.connect()
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async sendMessage(message: any): Promise<boolean> {
|
||||
if (!this.ws || !this.isConnected) {
|
||||
console.error('WebSocket is not connected')
|
||||
|
||||
// 如果启用了自动重连,尝试重连后再发送
|
||||
if (this.autoReconnect) {
|
||||
const reconnected = await this.attemptReconnect()
|
||||
if (!reconnected) return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ws?.send(message)
|
||||
console.log('Message sent:', message)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error)
|
||||
|
||||
// 如果发送失败,可能连接已关闭
|
||||
this.isConnected = false
|
||||
|
||||
// 如果启用了自动重连,尝试重连后再发送
|
||||
if (this.autoReconnect) {
|
||||
const reconnected = await this.attemptReconnect()
|
||||
if (reconnected) {
|
||||
// 重连成功,重试发送消息
|
||||
return this.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息监听器
|
||||
addListener(callback: (msg: Message) => void): void {
|
||||
// 保存监听器以便重连时重新添加
|
||||
this.listeners.push(callback)
|
||||
|
||||
if (!this.ws) {
|
||||
console.warn('WebSocket is not connected, listener will be added after connection')
|
||||
return
|
||||
}
|
||||
|
||||
this.ws.addListener(callback)
|
||||
}
|
||||
|
||||
|
||||
// 断开 WebSocket 连接
|
||||
async disconnect(): Promise<boolean> {
|
||||
if (!this.ws) {
|
||||
console.warn('WebSocket is not connected')
|
||||
this.isConnected = false
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ws.disconnect()
|
||||
this.isConnected = false
|
||||
this.ws = null
|
||||
console.log('Disconnected')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect:', error)
|
||||
// 即使断开连接失败,也将状态设置为未连接
|
||||
this.isConnected = false
|
||||
this.ws = null
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查连接状态
|
||||
isWebSocketConnected(): boolean {
|
||||
return this.isConnected && this.ws !== null
|
||||
}
|
||||
|
||||
// 设置自动重连选项
|
||||
setAutoReconnect(enable: boolean): void {
|
||||
this.autoReconnect = enable
|
||||
}
|
||||
|
||||
// 设置最大重连尝试次数
|
||||
setMaxReconnectAttempts(attempts: number): void {
|
||||
this.maxReconnectAttempts = attempts
|
||||
}
|
||||
|
||||
// 设置重连延迟
|
||||
setReconnectDelay(delay: number): void {
|
||||
this.reconnectDelay = delay
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
/*
|
||||
const webSocketClient = new WebSocketClient(
|
||||
'ws://127.0.0.1:8080',
|
||||
{}, // 连接配置
|
||||
{
|
||||
autoReconnect: true,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 1000
|
||||
}
|
||||
);
|
||||
|
||||
async function init() {
|
||||
// 连接
|
||||
const connected = await webSocketClient.connect();
|
||||
|
||||
if (connected) {
|
||||
// 添加消息监听器
|
||||
webSocketClient.addListener((msg) => {
|
||||
console.log('Received Message:', msg);
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
await webSocketClient.sendMessage('Hello World!');
|
||||
}
|
||||
}
|
||||
|
||||
// 在应用关闭时断开连接
|
||||
async function cleanup() {
|
||||
await webSocketClient.disconnect();
|
||||
}
|
||||
|
||||
init();
|
||||
*/
|
||||
10
src/vite-env.d.ts
vendored
10
src/vite-env.d.ts
vendored
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user