mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-04-12 23:50:31 +08:00
feat: port network topology visualization to network page
This commit is contained in:
parent
e4110f65c2
commit
0f1cd352db
@ -34,12 +34,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/plist": "^3.0.5",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.14.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"croner": "^9.1.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"express": "^5.2.1",
|
||||
"file-icon": "^6.0.0",
|
||||
"file-icon-info": "^1.1.1",
|
||||
|
||||
529
pnpm-lock.yaml
generated
529
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
||||
'@electron-toolkit/utils':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(electron@37.10.0)
|
||||
'@types/d3':
|
||||
specifier: ^7.4.3
|
||||
version: 7.4.3
|
||||
'@types/plist':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
@ -29,6 +32,9 @@ importers:
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
d3:
|
||||
specifier: ^7.9.0
|
||||
version: 7.9.0
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
@ -2173,6 +2179,99 @@ packages:
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/d3-array@3.2.2':
|
||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||
|
||||
'@types/d3-axis@3.0.6':
|
||||
resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
|
||||
|
||||
'@types/d3-brush@3.0.6':
|
||||
resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
|
||||
|
||||
'@types/d3-chord@3.0.6':
|
||||
resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
|
||||
|
||||
'@types/d3-color@3.1.3':
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
|
||||
'@types/d3-contour@3.0.6':
|
||||
resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
|
||||
|
||||
'@types/d3-delaunay@6.0.4':
|
||||
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
|
||||
|
||||
'@types/d3-dispatch@3.0.7':
|
||||
resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||
|
||||
'@types/d3-dsv@3.0.7':
|
||||
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
|
||||
|
||||
'@types/d3-ease@3.0.2':
|
||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||
|
||||
'@types/d3-fetch@3.0.7':
|
||||
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
|
||||
|
||||
'@types/d3-force@3.0.10':
|
||||
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
|
||||
|
||||
'@types/d3-format@3.0.4':
|
||||
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
|
||||
|
||||
'@types/d3-geo@3.1.0':
|
||||
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
|
||||
|
||||
'@types/d3-hierarchy@3.1.7':
|
||||
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||
|
||||
'@types/d3-path@3.1.1':
|
||||
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
||||
|
||||
'@types/d3-polygon@3.0.2':
|
||||
resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
|
||||
|
||||
'@types/d3-quadtree@3.0.6':
|
||||
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
|
||||
|
||||
'@types/d3-random@3.0.3':
|
||||
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
|
||||
|
||||
'@types/d3-scale-chromatic@3.1.0':
|
||||
resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
|
||||
|
||||
'@types/d3-scale@4.0.9':
|
||||
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
|
||||
|
||||
'@types/d3-selection@3.0.11':
|
||||
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||
|
||||
'@types/d3-shape@3.1.8':
|
||||
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
|
||||
|
||||
'@types/d3-time-format@4.0.3':
|
||||
resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
|
||||
|
||||
'@types/d3-time@3.0.4':
|
||||
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
|
||||
|
||||
'@types/d3-timer@3.0.2':
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||
|
||||
'@types/d3@7.4.3':
|
||||
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
||||
|
||||
@ -2191,6 +2290,9 @@ packages:
|
||||
'@types/fs-extra@9.0.13':
|
||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||
|
||||
'@types/geojson@7946.0.16':
|
||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
@ -2691,6 +2793,10 @@ packages:
|
||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commander@7.2.0:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
commander@9.5.0:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
@ -2760,6 +2866,133 @@ packages:
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-axis@3.0.0:
|
||||
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-brush@3.0.0:
|
||||
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-chord@3.0.1:
|
||||
resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-contour@4.0.2:
|
||||
resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-delaunay@6.0.4:
|
||||
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dispatch@3.0.1:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dsv@3.0.1:
|
||||
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-fetch@3.0.1:
|
||||
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-force@3.0.0:
|
||||
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-format@3.1.2:
|
||||
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-geo@3.1.1:
|
||||
resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-hierarchy@3.1.2:
|
||||
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-path@3.1.0:
|
||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-polygon@3.0.1:
|
||||
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-quadtree@3.0.1:
|
||||
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-random@3.0.1:
|
||||
resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-scale-chromatic@3.1.0:
|
||||
resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-scale@4.0.2:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time@3.1.0:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-transition@3.0.1:
|
||||
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
d3-selection: 2 - 3
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3@7.9.0:
|
||||
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
@ -2828,6 +3061,9 @@ packages:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
delaunator@5.1.0:
|
||||
resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@ -3536,6 +3772,10 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
intl-messageformat@10.7.18:
|
||||
resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==}
|
||||
|
||||
@ -4598,6 +4838,9 @@ packages:
|
||||
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
robust-predicates@3.0.3:
|
||||
resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
|
||||
|
||||
rollup@4.60.0:
|
||||
resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@ -4607,6 +4850,9 @@ packages:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
rw@1.3.3:
|
||||
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
|
||||
|
||||
safe-array-concat@1.1.3:
|
||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
engines: {node: '>=0.4'}
|
||||
@ -7888,6 +8134,123 @@ snapshots:
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/d3-array@3.2.2': {}
|
||||
|
||||
'@types/d3-axis@3.0.6':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-brush@3.0.6':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-chord@3.0.6': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
'@types/d3-contour@3.0.6':
|
||||
dependencies:
|
||||
'@types/d3-array': 3.2.2
|
||||
'@types/geojson': 7946.0.16
|
||||
|
||||
'@types/d3-delaunay@6.0.4': {}
|
||||
|
||||
'@types/d3-dispatch@3.0.7': {}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-dsv@3.0.7': {}
|
||||
|
||||
'@types/d3-ease@3.0.2': {}
|
||||
|
||||
'@types/d3-fetch@3.0.7':
|
||||
dependencies:
|
||||
'@types/d3-dsv': 3.0.7
|
||||
|
||||
'@types/d3-force@3.0.10': {}
|
||||
|
||||
'@types/d3-format@3.0.4': {}
|
||||
|
||||
'@types/d3-geo@3.1.0':
|
||||
dependencies:
|
||||
'@types/geojson': 7946.0.16
|
||||
|
||||
'@types/d3-hierarchy@3.1.7': {}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
dependencies:
|
||||
'@types/d3-color': 3.1.3
|
||||
|
||||
'@types/d3-path@3.1.1': {}
|
||||
|
||||
'@types/d3-polygon@3.0.2': {}
|
||||
|
||||
'@types/d3-quadtree@3.0.6': {}
|
||||
|
||||
'@types/d3-random@3.0.3': {}
|
||||
|
||||
'@types/d3-scale-chromatic@3.1.0': {}
|
||||
|
||||
'@types/d3-scale@4.0.9':
|
||||
dependencies:
|
||||
'@types/d3-time': 3.0.4
|
||||
|
||||
'@types/d3-selection@3.0.11': {}
|
||||
|
||||
'@types/d3-shape@3.1.8':
|
||||
dependencies:
|
||||
'@types/d3-path': 3.1.1
|
||||
|
||||
'@types/d3-time-format@4.0.3': {}
|
||||
|
||||
'@types/d3-time@3.0.4': {}
|
||||
|
||||
'@types/d3-timer@3.0.2': {}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
dependencies:
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3@7.4.3':
|
||||
dependencies:
|
||||
'@types/d3-array': 3.2.2
|
||||
'@types/d3-axis': 3.0.6
|
||||
'@types/d3-brush': 3.0.6
|
||||
'@types/d3-chord': 3.0.6
|
||||
'@types/d3-color': 3.1.3
|
||||
'@types/d3-contour': 3.0.6
|
||||
'@types/d3-delaunay': 6.0.4
|
||||
'@types/d3-dispatch': 3.0.7
|
||||
'@types/d3-drag': 3.0.7
|
||||
'@types/d3-dsv': 3.0.7
|
||||
'@types/d3-ease': 3.0.2
|
||||
'@types/d3-fetch': 3.0.7
|
||||
'@types/d3-force': 3.0.10
|
||||
'@types/d3-format': 3.0.4
|
||||
'@types/d3-geo': 3.1.0
|
||||
'@types/d3-hierarchy': 3.1.7
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-path': 3.1.1
|
||||
'@types/d3-polygon': 3.0.2
|
||||
'@types/d3-quadtree': 3.0.6
|
||||
'@types/d3-random': 3.0.3
|
||||
'@types/d3-scale': 4.0.9
|
||||
'@types/d3-scale-chromatic': 3.1.0
|
||||
'@types/d3-selection': 3.0.11
|
||||
'@types/d3-shape': 3.1.8
|
||||
'@types/d3-time': 3.0.4
|
||||
'@types/d3-time-format': 4.0.3
|
||||
'@types/d3-timer': 3.0.2
|
||||
'@types/d3-transition': 3.0.9
|
||||
'@types/d3-zoom': 3.0.8
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@ -7915,6 +8278,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@ -8540,6 +8905,8 @@ snapshots:
|
||||
|
||||
commander@5.1.0: {}
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
commander@9.5.0:
|
||||
optional: true
|
||||
|
||||
@ -8593,6 +8960,158 @@ snapshots:
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
d3-array@3.2.4:
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
|
||||
d3-axis@3.0.0: {}
|
||||
|
||||
d3-brush@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
d3-chord@3.0.1:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-contour@4.0.2:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-delaunay@6.0.4:
|
||||
dependencies:
|
||||
delaunator: 5.1.0
|
||||
|
||||
d3-dispatch@3.0.1: {}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
|
||||
d3-dsv@3.0.1:
|
||||
dependencies:
|
||||
commander: 7.2.0
|
||||
iconv-lite: 0.6.3
|
||||
rw: 1.3.3
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-fetch@3.0.1:
|
||||
dependencies:
|
||||
d3-dsv: 3.0.1
|
||||
|
||||
d3-force@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-quadtree: 3.0.1
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-format@3.1.2: {}
|
||||
|
||||
d3-geo@3.1.1:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-hierarchy@3.1.2: {}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
|
||||
d3-path@3.1.0: {}
|
||||
|
||||
d3-polygon@3.0.1: {}
|
||||
|
||||
d3-quadtree@3.0.1: {}
|
||||
|
||||
d3-random@3.0.1: {}
|
||||
|
||||
d3-scale-chromatic@3.1.0:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-interpolate: 3.0.1
|
||||
|
||||
d3-scale@4.0.2:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-format: 3.1.2
|
||||
d3-interpolate: 3.0.1
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
|
||||
d3-selection@3.0.0: {}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
dependencies:
|
||||
d3-time: 3.1.0
|
||||
|
||||
d3-time@3.1.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-dispatch: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
d3@7.9.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-axis: 3.0.0
|
||||
d3-brush: 3.0.0
|
||||
d3-chord: 3.0.1
|
||||
d3-color: 3.1.0
|
||||
d3-contour: 4.0.2
|
||||
d3-delaunay: 6.0.4
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-dsv: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-fetch: 3.0.1
|
||||
d3-force: 3.0.0
|
||||
d3-format: 3.1.2
|
||||
d3-geo: 3.1.1
|
||||
d3-hierarchy: 3.1.2
|
||||
d3-interpolate: 3.0.1
|
||||
d3-path: 3.1.0
|
||||
d3-polygon: 3.0.1
|
||||
d3-quadtree: 3.0.1
|
||||
d3-random: 3.0.1
|
||||
d3-scale: 4.0.2
|
||||
d3-scale-chromatic: 3.1.0
|
||||
d3-selection: 3.0.0
|
||||
d3-shape: 3.2.0
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
d3-timer: 3.0.1
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
@ -8655,6 +9174,10 @@ snapshots:
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
delaunator@5.1.0:
|
||||
dependencies:
|
||||
robust-predicates: 3.0.3
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
@ -9654,6 +10177,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
intl-messageformat@10.7.18:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
@ -10833,6 +11358,8 @@ snapshots:
|
||||
sprintf-js: 1.1.3
|
||||
optional: true
|
||||
|
||||
robust-predicates@3.0.3: {}
|
||||
|
||||
rollup@4.60.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@ -10874,6 +11401,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
rw@1.3.3: {}
|
||||
|
||||
safe-array-concat@1.1.3:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
|
||||
620
src/renderer/src/components/network/network-topology.tsx
Normal file
620
src/renderer/src/components/network/network-topology.tsx
Normal file
@ -0,0 +1,620 @@
|
||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
import { Button } from '@heroui/react'
|
||||
import { IoPauseOutline, IoPlayOutline, IoGitNetworkOutline } from 'react-icons/io5'
|
||||
import {
|
||||
IoDesktopOutline,
|
||||
IoServerOutline,
|
||||
IoFunnelOutline
|
||||
} from 'react-icons/io5'
|
||||
import { MdTag } from 'react-icons/md'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type NodeType = 'root' | 'client' | 'port' | 'rule' | 'group' | 'proxy'
|
||||
|
||||
interface TopologyNodeData {
|
||||
id: string
|
||||
name: string
|
||||
type: NodeType
|
||||
connections: number
|
||||
traffic: number
|
||||
children?: TopologyNodeData[]
|
||||
_children?: TopologyNodeData[]
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
// ─── Color helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function getNodeColors() {
|
||||
return {
|
||||
root: {
|
||||
fill: 'hsl(var(--heroui-default-500))',
|
||||
bg: 'hsl(var(--heroui-default-500) / 0.15)'
|
||||
},
|
||||
client: {
|
||||
fill: 'hsl(var(--heroui-primary))',
|
||||
bg: 'hsl(var(--heroui-primary) / 0.15)'
|
||||
},
|
||||
port: {
|
||||
fill: 'hsl(var(--heroui-warning))',
|
||||
bg: 'hsl(var(--heroui-warning) / 0.15)'
|
||||
},
|
||||
rule: {
|
||||
fill: 'hsl(var(--heroui-secondary))',
|
||||
bg: 'hsl(var(--heroui-secondary) / 0.15)'
|
||||
},
|
||||
group: {
|
||||
fill: 'hsl(var(--heroui-success))',
|
||||
bg: 'hsl(var(--heroui-success) / 0.15)'
|
||||
},
|
||||
proxy: {
|
||||
fill: 'hsl(var(--heroui-danger))',
|
||||
bg: 'hsl(var(--heroui-danger) / 0.15)'
|
||||
},
|
||||
baseContent: 'hsl(var(--heroui-foreground))'
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Hierarchy builder ────────────────────────────────────────────────────────
|
||||
|
||||
function buildHierarchy(
|
||||
connections: IMihomoConnectionDetail[],
|
||||
collapsedNodes: Set<string>
|
||||
): TopologyNodeData {
|
||||
const groupsMap = new Map<
|
||||
string,
|
||||
{
|
||||
data: TopologyNodeData
|
||||
proxies: Map<
|
||||
string,
|
||||
{
|
||||
data: TopologyNodeData
|
||||
rules: Map<
|
||||
string,
|
||||
{
|
||||
data: TopologyNodeData
|
||||
clients: Map<string, { data: TopologyNodeData; ports: Map<string, TopologyNodeData> }>
|
||||
}
|
||||
>
|
||||
}
|
||||
>
|
||||
}
|
||||
>()
|
||||
|
||||
for (const conn of connections) {
|
||||
const clientIP = conn.metadata.sourceIP || 'Unknown'
|
||||
const sourcePort = String(conn.metadata.sourcePort || 'Unknown')
|
||||
const ruleType = conn.rule || 'Direct'
|
||||
const fullRule = conn.rulePayload ? `${ruleType}: ${conn.rulePayload}` : ruleType
|
||||
const chains = conn.chains || []
|
||||
const proxy = chains[0] ?? 'Direct'
|
||||
const group = chains.length > 1 ? (chains[1] ?? 'Direct') : (chains[0] ?? 'Direct')
|
||||
const traffic = conn.download + conn.upload
|
||||
|
||||
if (!groupsMap.has(group)) {
|
||||
groupsMap.set(group, {
|
||||
data: { id: `group-${group}`, name: group, type: 'group', connections: 0, traffic: 0 },
|
||||
proxies: new Map()
|
||||
})
|
||||
}
|
||||
const groupEntry = groupsMap.get(group)!
|
||||
groupEntry.data.connections++
|
||||
groupEntry.data.traffic += traffic
|
||||
|
||||
if (!groupEntry.proxies.has(proxy)) {
|
||||
groupEntry.proxies.set(proxy, {
|
||||
data: {
|
||||
id: `proxy-${group}-${proxy}`,
|
||||
name: proxy,
|
||||
type: 'proxy',
|
||||
connections: 0,
|
||||
traffic: 0
|
||||
},
|
||||
rules: new Map()
|
||||
})
|
||||
}
|
||||
const proxyEntry = groupEntry.proxies.get(proxy)!
|
||||
proxyEntry.data.connections++
|
||||
proxyEntry.data.traffic += traffic
|
||||
|
||||
if (!proxyEntry.rules.has(fullRule)) {
|
||||
proxyEntry.rules.set(fullRule, {
|
||||
data: {
|
||||
id: `rule-${group}-${proxy}-${fullRule}`,
|
||||
name: fullRule,
|
||||
type: 'rule',
|
||||
connections: 0,
|
||||
traffic: 0
|
||||
},
|
||||
clients: new Map()
|
||||
})
|
||||
}
|
||||
const ruleEntry = proxyEntry.rules.get(fullRule)!
|
||||
ruleEntry.data.connections++
|
||||
ruleEntry.data.traffic += traffic
|
||||
|
||||
if (!ruleEntry.clients.has(clientIP)) {
|
||||
ruleEntry.clients.set(clientIP, {
|
||||
data: {
|
||||
id: `client-${group}-${proxy}-${fullRule}-${clientIP}`,
|
||||
name: clientIP,
|
||||
type: 'client',
|
||||
connections: 0,
|
||||
traffic: 0
|
||||
},
|
||||
ports: new Map()
|
||||
})
|
||||
}
|
||||
const clientEntry = ruleEntry.clients.get(clientIP)!
|
||||
clientEntry.data.connections++
|
||||
clientEntry.data.traffic += traffic
|
||||
|
||||
if (!clientEntry.ports.has(sourcePort)) {
|
||||
clientEntry.ports.set(sourcePort, {
|
||||
id: `port-${group}-${proxy}-${fullRule}-${clientIP}-${sourcePort}`,
|
||||
name: sourcePort,
|
||||
type: 'port',
|
||||
connections: 0,
|
||||
traffic: 0
|
||||
})
|
||||
}
|
||||
const portNode = clientEntry.ports.get(sourcePort)!
|
||||
portNode.connections++
|
||||
portNode.traffic += traffic
|
||||
}
|
||||
|
||||
// Convert to tree with collapse state
|
||||
const rootChildren: TopologyNodeData[] = []
|
||||
|
||||
function applyCollapse(node: TopologyNodeData, defaultCollapsed = false): TopologyNodeData {
|
||||
const isCollapsed = collapsedNodes.has(node.id) || defaultCollapsed
|
||||
if (isCollapsed && node.children && node.children.length > 0) {
|
||||
return { ...node, _children: node.children, children: undefined, collapsed: true }
|
||||
}
|
||||
return { ...node, collapsed: false }
|
||||
}
|
||||
|
||||
groupsMap.forEach((groupEntry) => {
|
||||
const groupNode: TopologyNodeData = { ...groupEntry.data, children: [] }
|
||||
|
||||
groupEntry.proxies.forEach((proxyEntry) => {
|
||||
const proxyNode: TopologyNodeData = { ...proxyEntry.data, children: [] }
|
||||
|
||||
proxyEntry.rules.forEach((ruleEntry) => {
|
||||
const ruleNode: TopologyNodeData = { ...ruleEntry.data, children: [] }
|
||||
|
||||
ruleEntry.clients.forEach((clientEntry) => {
|
||||
const portChildren = Array.from(clientEntry.ports.values())
|
||||
const isClientCollapsed = !collapsedNodes.has(`expanded-${clientEntry.data.id}`)
|
||||
const clientNode: TopologyNodeData = isClientCollapsed
|
||||
? { ...clientEntry.data, _children: portChildren, children: undefined, collapsed: true }
|
||||
: { ...clientEntry.data, children: portChildren, collapsed: false }
|
||||
ruleNode.children!.push(clientNode)
|
||||
})
|
||||
|
||||
const isRuleCollapsed = !collapsedNodes.has(`expanded-${ruleEntry.data.id}`)
|
||||
if (isRuleCollapsed && ruleNode.children!.length > 0) {
|
||||
ruleNode._children = ruleNode.children
|
||||
ruleNode.children = undefined
|
||||
ruleNode.collapsed = true
|
||||
} else {
|
||||
ruleNode.collapsed = false
|
||||
}
|
||||
proxyNode.children!.push(ruleNode)
|
||||
})
|
||||
|
||||
groupNode.children!.push(applyCollapse(proxyNode))
|
||||
})
|
||||
|
||||
rootChildren.push(applyCollapse(groupNode))
|
||||
})
|
||||
|
||||
return {
|
||||
id: 'root',
|
||||
name: 'Connections',
|
||||
type: 'root',
|
||||
connections: connections.length,
|
||||
traffic: connections.reduce((s, c) => s + c.download + c.upload, 0),
|
||||
children: rootChildren
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Text measurement ─────────────────────────────────────────────────────────
|
||||
|
||||
let measureCanvas: HTMLCanvasElement | null = null
|
||||
function getTextWidth(text: string, font = '600 11px sans-serif'): number {
|
||||
if (!measureCanvas) measureCanvas = document.createElement('canvas')
|
||||
const ctx = measureCanvas.getContext('2d')
|
||||
if (!ctx) return text.length * 7
|
||||
ctx.font = font
|
||||
return ctx.measureText(text).width
|
||||
}
|
||||
|
||||
// ─── Main component ───────────────────────────────────────────────────────────
|
||||
|
||||
const NetworkTopologyCard: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
const svgRef = useRef<SVGSVGElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [connections, setConnections] = useState<IMihomoConnectionDetail[]>([])
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
const frozenRef = useRef<IMihomoConnectionDetail[] | null>(null)
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
|
||||
|
||||
// IPC listener
|
||||
useEffect(() => {
|
||||
if (isPaused) return
|
||||
const handler = (_e: unknown, ...args: unknown[]): void => {
|
||||
const info = args[0] as IMihomoConnectionsInfo
|
||||
setConnections(info.connections ?? [])
|
||||
}
|
||||
window.electron.ipcRenderer.on('mihomoConnections', handler)
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
||||
}
|
||||
}, [isPaused])
|
||||
|
||||
const currentConnections = isPaused && frozenRef.current ? frozenRef.current : connections
|
||||
|
||||
// Stats
|
||||
const stats = useMemo(() => {
|
||||
const clients = new Set<string>()
|
||||
const rules = new Set<string>()
|
||||
const groups = new Set<string>()
|
||||
const proxies = new Set<string>()
|
||||
for (const c of currentConnections) {
|
||||
clients.add(c.metadata.sourceIP || 'Unknown')
|
||||
rules.add(c.rule || 'Direct')
|
||||
const ch = c.chains || []
|
||||
proxies.add(ch[0] ?? 'Direct')
|
||||
groups.add(ch.length > 1 ? (ch[1] ?? 'Direct') : (ch[0] ?? 'Direct'))
|
||||
}
|
||||
return {
|
||||
clientCount: clients.size,
|
||||
ruleCount: rules.size,
|
||||
groupCount: groups.size,
|
||||
proxyCount: proxies.size,
|
||||
totalTraffic: currentConnections.reduce((s, c) => s + c.download + c.upload, 0)
|
||||
}
|
||||
}, [currentConnections])
|
||||
|
||||
// Hierarchy data
|
||||
const hierarchyData = useMemo(
|
||||
() => buildHierarchy(currentConnections, collapsedNodes),
|
||||
[currentConnections, collapsedNodes]
|
||||
)
|
||||
|
||||
// Toggle collapse
|
||||
const toggleCollapseRef = useRef<(nodeId: string, isCollapsed: boolean) => void>(() => {})
|
||||
toggleCollapseRef.current = useCallback(
|
||||
(nodeId: string, isCurrentlyCollapsed: boolean) => {
|
||||
const expandedKey = `expanded-${nodeId}`
|
||||
setCollapsedNodes((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (isCurrentlyCollapsed) {
|
||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||
next.add(expandedKey)
|
||||
} else {
|
||||
next.delete(nodeId)
|
||||
}
|
||||
} else {
|
||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||
next.delete(expandedKey)
|
||||
} else {
|
||||
next.add(nodeId)
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// D3 render
|
||||
useEffect(() => {
|
||||
const svgEl = svgRef.current
|
||||
const containerEl = containerRef.current
|
||||
if (!svgEl || !containerEl) return
|
||||
if (!hierarchyData.children || hierarchyData.children.length === 0) {
|
||||
d3.select(svgEl).selectAll('*').remove()
|
||||
return
|
||||
}
|
||||
|
||||
d3.select(svgEl).selectAll('*').remove()
|
||||
|
||||
const nodeHeight = 30
|
||||
const nodePaddingX = 16
|
||||
const nodeSpacingY = 50
|
||||
const levelGap = 40
|
||||
const topPadding = 25
|
||||
|
||||
const root = d3.hierarchy(hierarchyData)
|
||||
const containerWidth = containerEl.clientWidth
|
||||
|
||||
const treeLayout = d3
|
||||
.tree<TopologyNodeData>()
|
||||
.nodeSize([nodeSpacingY, 100])
|
||||
.separation(() => 1)
|
||||
|
||||
treeLayout(root)
|
||||
|
||||
// Calculate node widths
|
||||
const nodeWidths = new Map<string, number>()
|
||||
for (const d of root.descendants()) {
|
||||
if (d.data.type !== 'root') {
|
||||
const textWidth = getTextWidth(d.data.name)
|
||||
const hasChildren =
|
||||
(d.data.children && d.data.children.length > 0) ||
|
||||
(d.data._children && d.data._children.length > 0)
|
||||
nodeWidths.set(d.data.id, textWidth + nodePaddingX * 2 + (hasChildren ? 20 : 0))
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeWidth = (d: d3.HierarchyNode<TopologyNodeData>) =>
|
||||
nodeWidths.get(d.data.id) ?? 80
|
||||
|
||||
// Max width per depth
|
||||
const maxWidthPerLevel = new Map<number, number>()
|
||||
root.descendants().forEach((d) => {
|
||||
if (d.data.type !== 'root') {
|
||||
const w = getNodeWidth(d)
|
||||
maxWidthPerLevel.set(d.depth, Math.max(maxWidthPerLevel.get(d.depth) ?? 0, w))
|
||||
}
|
||||
})
|
||||
|
||||
// Cumulative x offsets
|
||||
const levelXOffset = new Map<number, number>()
|
||||
let cumX = 0
|
||||
for (let depth = 1; depth <= maxWidthPerLevel.size; depth++) {
|
||||
const curW = maxWidthPerLevel.get(depth) ?? 100
|
||||
if (depth === 1) {
|
||||
cumX = curW / 2
|
||||
} else {
|
||||
const prevW = maxWidthPerLevel.get(depth - 1) ?? 100
|
||||
cumX += prevW / 2 + levelGap + curW / 2
|
||||
}
|
||||
levelXOffset.set(depth, cumX)
|
||||
}
|
||||
|
||||
root.descendants().forEach((d) => {
|
||||
if (d.data.type !== 'root' && d.depth > 0) {
|
||||
d.y = levelXOffset.get(d.depth) ?? d.y
|
||||
}
|
||||
})
|
||||
|
||||
// Bounds
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
root.each((d) => {
|
||||
if ((d.x ?? 0) < minX) minX = d.x ?? 0
|
||||
if ((d.x ?? 0) > maxX) maxX = d.x ?? 0
|
||||
})
|
||||
|
||||
const treeHeight = maxX - minX + nodeSpacingY
|
||||
const actualHeight = Math.max(400, treeHeight + topPadding + 40)
|
||||
|
||||
let maxY = 0
|
||||
root.each((d) => {
|
||||
if (d.data.type !== 'root') {
|
||||
const rightEdge = (d.y ?? 0) + getNodeWidth(d) / 2
|
||||
if (rightEdge > maxY) maxY = rightEdge
|
||||
}
|
||||
})
|
||||
const actualWidth = Math.max(containerWidth, maxY + 60 + 40)
|
||||
|
||||
const svg = d3.select(svgEl).attr('width', actualWidth).attr('height', actualHeight)
|
||||
|
||||
const g = svg
|
||||
.append('g')
|
||||
.attr('transform', `translate(60, ${-minX + nodeSpacingY / 2 + topPadding})`)
|
||||
|
||||
const colors = getNodeColors()
|
||||
|
||||
// Links
|
||||
const visibleLinks = root.links().filter((l) => l.source.data.type !== 'root')
|
||||
g.selectAll('.link')
|
||||
.data(visibleLinks)
|
||||
.join('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', (d) => {
|
||||
const src = d.source as d3.HierarchyPointNode<TopologyNodeData>
|
||||
const tgt = d.target as d3.HierarchyPointNode<TopologyNodeData>
|
||||
const sx = src.y + getNodeWidth(src) / 2
|
||||
const sy = src.x
|
||||
const tx = tgt.y - getNodeWidth(tgt) / 2
|
||||
const ty = tgt.x
|
||||
const mx = (sx + tx) / 2
|
||||
return `M${sx},${sy} C${mx},${sy} ${mx},${ty} ${tx},${ty}`
|
||||
})
|
||||
.attr('fill', 'none')
|
||||
.style('stroke', colors.baseContent)
|
||||
.attr('stroke-opacity', 0.3)
|
||||
.attr('stroke-width', (d) => Math.max(1, Math.min(4, d.target.data.connections / 5)))
|
||||
|
||||
// Nodes
|
||||
const nodes = g
|
||||
.selectAll('.node')
|
||||
.data(root.descendants().filter((d) => d.data.type !== 'root'))
|
||||
.join('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', (d) => `translate(${d.y ?? 0},${d.x})`)
|
||||
.style('cursor', (d) => {
|
||||
const hasChildren =
|
||||
(d.data.children && d.data.children.length > 0) ||
|
||||
(d.data._children && d.data._children.length > 0)
|
||||
return hasChildren ? 'pointer' : 'default'
|
||||
})
|
||||
.on('click', (_event, d) => {
|
||||
const hasChildren =
|
||||
(d.data.children && d.data.children.length > 0) ||
|
||||
(d.data._children && d.data._children.length > 0)
|
||||
if (hasChildren) {
|
||||
toggleCollapseRef.current(d.data.id, d.data.collapsed ?? false)
|
||||
}
|
||||
})
|
||||
|
||||
// Connection count badge
|
||||
nodes
|
||||
.append('text')
|
||||
.attr('dy', -nodeHeight / 2 - 4)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('fill', (d) => colors[d.data.type].fill)
|
||||
.attr('font-size', '10px')
|
||||
.attr('font-weight', '500')
|
||||
.text((d) => `${d.data.connections}`)
|
||||
|
||||
// Node rect
|
||||
nodes
|
||||
.append('rect')
|
||||
.attr('x', (d) => -getNodeWidth(d) / 2)
|
||||
.attr('y', -nodeHeight / 2)
|
||||
.attr('width', (d) => getNodeWidth(d))
|
||||
.attr('height', nodeHeight)
|
||||
.attr('rx', 6)
|
||||
.style('fill', (d) => colors[d.data.type].bg)
|
||||
.style('stroke', (d) => colors[d.data.type].fill)
|
||||
.attr('stroke-width', 2)
|
||||
|
||||
// Collapse indicator
|
||||
nodes
|
||||
.filter((d) =>
|
||||
Boolean(
|
||||
(d.data.children && d.data.children.length > 0) ||
|
||||
(d.data._children && d.data._children.length > 0)
|
||||
)
|
||||
)
|
||||
.append('text')
|
||||
.attr('x', (d) => getNodeWidth(d) / 2 - 12)
|
||||
.attr('dy', '0.35em')
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('fill', (d) => colors[d.data.type].fill)
|
||||
.attr('font-size', '14px')
|
||||
.attr('font-weight', '700')
|
||||
.text((d) => (d.data.collapsed ? '+' : '−'))
|
||||
|
||||
// Label
|
||||
nodes
|
||||
.append('text')
|
||||
.attr('dy', '0.31em')
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('fill', (d) => colors[d.data.type].fill)
|
||||
.attr('font-size', '11px')
|
||||
.attr('font-weight', '600')
|
||||
.text((d) => d.data.name)
|
||||
|
||||
// Tooltip
|
||||
nodes
|
||||
.append('title')
|
||||
.text(
|
||||
(d) =>
|
||||
`${d.data.name}\n${d.data.connections} connections\n${calcTraffic(d.data.traffic)}`
|
||||
)
|
||||
}, [hierarchyData, resolvedTheme])
|
||||
|
||||
// ResizeObserver
|
||||
useEffect(() => {
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
const observer = new ResizeObserver(() => {
|
||||
// Re-trigger render by forcing a state touch isn't ideal;
|
||||
// instead just re-run the render imperatively
|
||||
if (svgRef.current && containerRef.current) {
|
||||
svgRef.current.setAttribute('width', String(containerRef.current.clientWidth))
|
||||
}
|
||||
})
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
const togglePause = useCallback(() => {
|
||||
if (isPaused) {
|
||||
frozenRef.current = null
|
||||
} else {
|
||||
frozenRef.current = [...connections]
|
||||
}
|
||||
setIsPaused((p) => !p)
|
||||
}, [isPaused, connections])
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-foreground/10 bg-content1 p-4 shadow-sm">
|
||||
{/* Header */}
|
||||
<div className="mb-3.5 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-success/15 text-success">
|
||||
<IoGitNetworkOutline size={18} />
|
||||
</div>
|
||||
<h3 className="text-[15px] font-semibold">{t('network.topology.title')}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Stats */}
|
||||
<div className="hidden flex-wrap gap-x-2 text-[12px] text-foreground/50 sm:flex">
|
||||
<span>{stats.clientCount} {t('network.topology.clients')}</span>
|
||||
<span>·</span>
|
||||
<span>{stats.ruleCount} {t('network.topology.rules')}</span>
|
||||
<span>·</span>
|
||||
<span>{stats.groupCount} {t('network.topology.groups')}</span>
|
||||
<span>·</span>
|
||||
<span>{stats.proxyCount} {t('network.topology.nodes')}</span>
|
||||
<span>·</span>
|
||||
<span>{calcTraffic(stats.totalTraffic)}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={togglePause}
|
||||
className={`h-7 w-7 min-w-0 ${isPaused ? 'text-warning' : ''}`}
|
||||
title={isPaused ? t('network.topology.resume') : t('network.topology.pause')}
|
||||
>
|
||||
{isPaused ? <IoPlayOutline size={16} /> : <IoPauseOutline size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mb-3 flex flex-wrap gap-3 text-[12px] text-foreground/60">
|
||||
<span className="flex items-center gap-1">
|
||||
<IoGitNetworkOutline className="text-success" size={13} />
|
||||
{t('network.topology.proxyGroups')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<IoServerOutline className="text-danger" size={13} />
|
||||
{t('network.topology.proxyNodes')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<IoFunnelOutline className="text-secondary" size={13} />
|
||||
{t('network.topology.rules')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<IoDesktopOutline className="text-primary" size={13} />
|
||||
{t('network.topology.sourceIP')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MdTag className="text-warning" size={13} />
|
||||
{t('network.topology.sourcePort')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{currentConnections.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-foreground/40">
|
||||
<IoGitNetworkOutline size={32} className="mb-2 animate-pulse" />
|
||||
<span className="text-sm">{t('network.topology.waiting')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div ref={containerRef} className="overflow-x-auto touch-pan-x touch-pan-y">
|
||||
<svg ref={svgRef} style={{ minHeight: '400px' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NetworkTopologyCard
|
||||
@ -695,5 +695,17 @@
|
||||
"guide.dns.title": "DNS OVRD",
|
||||
"guide.dns.description": "The software defaults to using the application's DNS settings to override the subscription configuration. If you need to use the DNS settings from the subscription configuration, please disable this feature. The same applies to domain sniffing.",
|
||||
"guide.end.title": "Tutorial Complete",
|
||||
"guide.end.description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news."
|
||||
}
|
||||
"guide.end.description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news.",
|
||||
"network.topology.title": "Network Topology",
|
||||
"network.topology.clients": "clients",
|
||||
"network.topology.rules": "Rules",
|
||||
"network.topology.groups": "groups",
|
||||
"network.topology.nodes": "nodes",
|
||||
"network.topology.proxyGroups": "Proxy Groups",
|
||||
"network.topology.proxyNodes": "Proxy Nodes",
|
||||
"network.topology.sourceIP": "Source IP",
|
||||
"network.topology.sourcePort": "Source Port",
|
||||
"network.topology.waiting": "Waiting for connections...",
|
||||
"network.topology.pause": "Pause",
|
||||
"network.topology.resume": "Resume"
|
||||
}
|
||||
@ -659,5 +659,17 @@
|
||||
"guide.dns.title": "DNS",
|
||||
"guide.dns.description": "نرمافزار بهطور پیشفرض از تنظیمات DNS برنامه برای بازنویسی پیکربندی اشتراک استفاده میکند. اگر نیاز به استفاده از تنظیمات DNS موجود در پیکربندی اشتراک دارید، لطفاً این قابلیت را غیرفعال کنید. همین موضوع برای شناسایی دامنه نیز صدق میکند.",
|
||||
"guide.end.title": "پایان آموزش",
|
||||
"guide.end.description": "اکنون که با استفاده اساسی از نرمافزار آشنا شدید، اشتراک خود را وارد کنید و از آن استفاده کنید. لذت ببرید!\nهمچنین میتوانید برای آخرین اخبار به <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">گروه تلگرام</a> ما بپیوندید."
|
||||
}
|
||||
"guide.end.description": "اکنون که با استفاده اساسی از نرمافزار آشنا شدید، اشتراک خود را وارد کنید و از آن استفاده کنید. لذت ببرید!\nهمچنین میتوانید برای آخرین اخبار به <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">گروه تلگرام</a> ما بپیوندید.",
|
||||
"network.topology.title": "توپولوژی شبکه",
|
||||
"network.topology.clients": "کلاینت",
|
||||
"network.topology.rules": "قوانین",
|
||||
"network.topology.groups": "گروهها",
|
||||
"network.topology.nodes": "گرهها",
|
||||
"network.topology.proxyGroups": "گروههای پروکسی",
|
||||
"network.topology.proxyNodes": "گرههای پروکسی",
|
||||
"network.topology.sourceIP": "IP منبع",
|
||||
"network.topology.sourcePort": "پورت منبع",
|
||||
"network.topology.waiting": "در انتظار اتصال...",
|
||||
"network.topology.pause": "مکث",
|
||||
"network.topology.resume": "ادامه"
|
||||
}
|
||||
@ -661,5 +661,17 @@
|
||||
"guide.dns.title": "DNS",
|
||||
"guide.dns.description": "Программное обеспечение по умолчанию использует настройки DNS приложения для переопределения конфигурации подписки. Если вам нужно использовать настройки DNS из конфигурации подписки, пожалуйста, отключите эту функцию. То же самое относится к определению домена.",
|
||||
"guide.end.title": "Руководство завершено",
|
||||
"guide.end.description": "Теперь, когда вы понимаете основы использования программы, импортируйте свою подписку и начните использовать ее. Приятного использования!\nВы также можете присоединиться к нашей официальной <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">группе Telegram</a> для получения последних новостей."
|
||||
}
|
||||
"guide.end.description": "Теперь, когда вы понимаете основы использования программы, импортируйте свою подписку и начните использовать ее. Приятного использования!\nВы также можете присоединиться к нашей официальной <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">группе Telegram</a> для получения последних новостей.",
|
||||
"network.topology.title": "Топология сети",
|
||||
"network.topology.clients": "клиентов",
|
||||
"network.topology.rules": "правил",
|
||||
"network.topology.groups": "групп",
|
||||
"network.topology.nodes": "узлов",
|
||||
"network.topology.proxyGroups": "Группы прокси",
|
||||
"network.topology.proxyNodes": "Узлы прокси",
|
||||
"network.topology.sourceIP": "IP источника",
|
||||
"network.topology.sourcePort": "Порт источника",
|
||||
"network.topology.waiting": "Ожидание подключений...",
|
||||
"network.topology.pause": "Пауза",
|
||||
"network.topology.resume": "Возобновить"
|
||||
}
|
||||
@ -695,5 +695,17 @@
|
||||
"guide.dns.title": "DNS",
|
||||
"guide.dns.description": "软件默认使用应用的 DNS 设置覆盖订阅配置,如果您需要使用订阅配置中的 DNS 设置,请关闭此功能,域名嗅探同理",
|
||||
"guide.end.title": "教程结束",
|
||||
"guide.end.description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群组</a> 获取最新资讯"
|
||||
}
|
||||
"guide.end.description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群组</a> 获取最新资讯",
|
||||
"network.topology.title": "网络拓扑",
|
||||
"network.topology.clients": "客户端",
|
||||
"network.topology.rules": "规则",
|
||||
"network.topology.groups": "代理组",
|
||||
"network.topology.nodes": "节点",
|
||||
"network.topology.proxyGroups": "代理组",
|
||||
"network.topology.proxyNodes": "代理节点",
|
||||
"network.topology.sourceIP": "来源 IP",
|
||||
"network.topology.sourcePort": "来源端口",
|
||||
"network.topology.waiting": "等待连接数据...",
|
||||
"network.topology.pause": "暂停",
|
||||
"network.topology.resume": "恢复"
|
||||
}
|
||||
@ -695,5 +695,17 @@
|
||||
"guide.dns.title": "DNS",
|
||||
"guide.dns.description": "軟件默認使用應用程式的 DNS 設置覆蓋訂閱配置,如果您需要使用訂閱配置中的 DNS 設置,請關閉此功能,域名嗅探同理",
|
||||
"guide.end.title": "教程結束",
|
||||
"guide.end.description": "現在您已經瞭解了軟件的基本用法,匯入您的訂閱開始使用吧,祝您使用愉快!\n您還可以加入我們的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群組</a> 獲取最新資訊"
|
||||
}
|
||||
"guide.end.description": "現在您已經瞭解了軟件的基本用法,匯入您的訂閱開始使用吧,祝您使用愉快!\n您還可以加入我們的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群組</a> 獲取最新資訊",
|
||||
"network.topology.title": "網路拓撲",
|
||||
"network.topology.clients": "客戶端",
|
||||
"network.topology.rules": "規則",
|
||||
"network.topology.groups": "代理群組",
|
||||
"network.topology.nodes": "節點",
|
||||
"network.topology.proxyGroups": "代理群組",
|
||||
"network.topology.proxyNodes": "代理節點",
|
||||
"network.topology.sourceIP": "來源 IP",
|
||||
"network.topology.sourcePort": "來源連接埠",
|
||||
"network.topology.waiting": "等待連線資料...",
|
||||
"network.topology.pause": "暫停",
|
||||
"network.topology.resume": "繼續"
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import BasePage from '@renderer/components/base/base-page'
|
||||
import NetworkTopologyCard from '@renderer/components/network/network-topology'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Button, Select, SelectItem, Chip, Tooltip } from '@heroui/react'
|
||||
import {
|
||||
@ -385,6 +386,9 @@ const IPPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 网络拓扑卡片 */}
|
||||
<NetworkTopologyCard />
|
||||
|
||||
{/* 网络延迟卡片 */}
|
||||
<div className="rounded-xl border border-foreground/10 bg-content1 p-4 shadow-sm">
|
||||
<div className="mb-3.5 flex items-center justify-between gap-3">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user