Compare commits

...
3 Commits
Author SHA1 Message Date
lbl 298c3965e2 chore: bump version to 2.0.2 2026-08-23 11:37:34 +08:00
lbl 619a5bcf18 feat(web): share network configs via QR code 2026-08-23 11:03:28 +08:00
lbl 1acb5e8a86 feat(jni): expose detailed peer metrics 2026-08-23 10:28:37 +08:00
15 changed files with 431 additions and 32 deletions
Generated
+6 -6
View File
@@ -6294,7 +6294,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vnt-core"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"anyhow",
"bytes",
@@ -6340,7 +6340,7 @@ dependencies = [
[[package]]
name = "vnt-desktop"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"anyhow",
"log",
@@ -6361,7 +6361,7 @@ dependencies = [
[[package]]
name = "vnt-ipc"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"anyhow",
"cli-table",
@@ -6379,7 +6379,7 @@ dependencies = [
[[package]]
name = "vnt-jni"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"anyhow",
"hostname",
@@ -6396,7 +6396,7 @@ dependencies = [
[[package]]
name = "vnt-web"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"anyhow",
"axum",
@@ -6421,7 +6421,7 @@ dependencies = [
[[package]]
name = "vnt2"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"anyhow",
"clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vnt2"
version = "2.0.1"
version = "2.0.2"
edition = "2024"
[dependencies]
+216
View File
@@ -74,6 +74,9 @@ importers:
pinia:
specifier: 'catalog:'
version: 3.0.4([email protected])
qrcode:
specifier: 1.5.4
version: 1.5.4
vue:
specifier: 'catalog:'
version: 3.5.41
@@ -661,9 +664,31 @@ packages:
'@vue/[email protected]':
resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==}
[email protected]:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
[email protected]:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
[email protected]:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
[email protected]:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
[email protected]:
resolution: {integrity: sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q==}
engines: {node: '>=18'}
@@ -671,10 +696,20 @@ packages:
[email protected]:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
[email protected]:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
[email protected]:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
[email protected]:
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
engines: {node: '>=10.13.0'}
@@ -700,17 +735,29 @@ packages:
picomatch:
optional: true
[email protected]:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
[email protected]:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
[email protected]:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
[email protected]:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
[email protected]:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@@ -789,6 +836,10 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
[email protected]:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -800,6 +851,22 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
[email protected]:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
@@ -819,10 +886,26 @@ packages:
typescript:
optional: true
[email protected]:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
[email protected]:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
[email protected]:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
[email protected]:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
@@ -831,6 +914,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
[email protected]:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -839,6 +925,14 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
engines: {node: '>=16'}
@@ -907,6 +1001,24 @@ packages:
typescript:
optional: true
[email protected]:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
[email protected]:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
[email protected]:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
snapshots:
'@babel/[email protected]': {}
@@ -1306,14 +1418,40 @@ snapshots:
'@vue/[email protected]': {}
[email protected]: {}
[email protected]:
dependencies:
color-convert: 2.0.1
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
[email protected]:
dependencies:
color-name: 1.1.4
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
graceful-fs: 4.2.11
@@ -1356,13 +1494,22 @@ snapshots:
optionalDependencies:
picomatch: 4.0.5
[email protected]:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
[email protected]:
optional: true
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -1414,6 +1561,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
[email protected]:
dependencies:
p-locate: 4.1.0
[email protected]:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1422,6 +1573,18 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
p-try: 2.2.0
[email protected]:
dependencies:
p-limit: 2.3.0
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -1433,12 +1596,24 @@ snapshots:
'@vue/devtools-api': 7.7.10
vue: 3.5.41
[email protected]: {}
[email protected]:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
[email protected]:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -1473,10 +1648,22 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.62.5
fsevents: 2.3.3
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
[email protected]:
dependencies:
ansi-regex: 5.0.1
[email protected]:
dependencies:
copy-anything: 4.1.0
@@ -1515,3 +1702,32 @@ snapshots:
'@vue/runtime-dom': 3.5.41
'@vue/server-renderer': 3.5.41
'@vue/shared': 3.5.41
[email protected]: {}
[email protected]:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
[email protected]: {}
[email protected]:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
[email protected]:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vnt-core"
version = "2.0.1"
version = "2.0.2"
edition = "2024"
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "vnt-desktop",
"private": true,
"version": "2.0.1",
"version": "2.0.2",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vnt-desktop"
version = "2.0.1"
version = "2.0.2"
description = "VNT virtual network desktop client"
edition = "2024"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "VNT Desktop",
"version": "2.0.1",
"version": "2.0.2",
"identifier": "com.vnt.desktop",
"build": {
"beforeDevCommand": "pnpm dev",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vnt-ipc"
version = "2.0.1"
version = "2.0.2"
edition = "2024"
[dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vnt-jni"
version = "2.0.1"
version = "2.0.2"
edition = "2024"
[lib]
+98 -12
View File
@@ -68,6 +68,16 @@ where
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_err(panic_message)
}
fn encryption_state(local_key: Option<&str>, peer_key: Option<&str>) -> i32 {
match (local_key, peer_key) {
(Some(local), Some(peer)) if local == peer => 1,
(None, None) => 2,
(Some(_), None) => 3,
(None, Some(_)) => 4,
(Some(_), Some(_)) => 5,
}
}
/// JNI 导出函数的 panic 防护:panic 时向 JVM 抛出异常并返回默认值
macro_rules! jni_guard {
($env:ident, $default_ret:expr, { $($body:tt)* }) => {{
@@ -479,21 +489,88 @@ pub extern "system" fn Java_com_vnt_VntApi_nativeGetClientList<'local>(
) -> jstring {
jni_guard!(env, std::ptr::null_mut(), {
let result: anyhow::Result<String> = (|| {
let global_state = GLOBAL_STATE.lock();
let state = global_state.as_ref().context("VNT not initialized")?;
let (api, runtime) = {
let global_state = GLOBAL_STATE.lock();
let state = global_state.as_ref().context("VNT not initialized")?;
(
state
.vnt_apis
.get(&api_handle)
.context("Invalid API handle")?
.clone(),
state.runtime.clone(),
)
};
let api = state
.vnt_apis
.get(&api_handle)
.context("Invalid API handle")?;
let client_ips = api.client_ips();
let json_array: Vec<_> = client_ips
let local_clients: HashMap<_, _> = api
.client_ips()
.into_iter()
.map(|client| {
.map(|client| (client.ip, client.online))
.collect();
let server_clients: HashMap<_, _> = runtime
.block_on(api.server_rpc().client_list())
.map(|response| {
response
.list
.into_iter()
.map(|client| (Ipv4Addr::from(client.ip), client))
.collect()
})
.unwrap_or_default();
let local_key = api.get_config().and_then(|config| config.key_sign());
let mut ips: Vec<_> = local_clients
.keys()
.chain(server_clients.keys())
.copied()
.collect();
ips.sort_unstable();
ips.dedup();
let json_array: Vec<_> = ips
.into_iter()
.map(|ip| {
let server_client = server_clients.get(&ip);
let route = api.find_route(&ip);
let has_route = route.is_some();
let direct = route
.as_ref()
.map(|route| route.metric() == 1)
.unwrap_or(false);
let route_protocol = route
.as_ref()
.map(|route| route.route_key().protocol().to_string());
let route_metric = route.as_ref().map(|route| route.metric());
let rtt = route.as_ref().map(|route| route.rtt());
let online = local_clients.get(&ip).copied().unwrap_or(false)
|| server_client.map(|client| client.online).unwrap_or(false)
|| has_route;
let packet_loss = api.packet_loss_info(&ip).map(|info| {
serde_json::json!({
"sent": info.sent,
"received": info.received,
"loss_rate": info.loss_rate,
})
});
let traffic = api.traffic_info(&ip).map(|info| {
serde_json::json!({
"tx_bytes": info.tx_bytes,
"rx_bytes": info.rx_bytes,
})
});
serde_json::json!({
"ip": client.ip.to_string(),
"online": client.online,
"ip": ip.to_string(),
"name": server_client.map(|client| client.name.as_str()).unwrap_or(""),
"version": server_client.map(|client| client.version.as_str()).unwrap_or(""),
"online": online,
"direct": direct,
"route_protocol": route_protocol,
"route_metric": route_metric,
"rtt": rtt,
"key_equal": server_client
.map(|client| encryption_state(local_key.as_deref(), client.key_sign.as_deref()))
.unwrap_or(0),
"packet_loss": packet_loss,
"traffic": traffic,
})
})
.collect();
@@ -1034,4 +1111,13 @@ mod tests {
let result: Result<(), String> = catch_jni_panic(|| panic!("{}", "kaboom"));
assert_eq!(result.unwrap_err(), "kaboom");
}
#[test]
fn maps_peer_encryption_states() {
assert_eq!(encryption_state(Some("same"), Some("same")), 1);
assert_eq!(encryption_state(None, None), 2);
assert_eq!(encryption_state(Some("local"), None), 3);
assert_eq!(encryption_state(None, Some("peer")), 4);
assert_eq!(encryption_state(Some("local"), Some("peer")), 5);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vnt-web"
version = "2.0.1"
version = "2.0.2"
edition = "2024"
[dependencies]
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "vnt-web-ui",
"private": true,
"version": "2.0.1",
"version": "2.0.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -9,6 +9,7 @@
},
"dependencies": {
"pinia": "catalog:",
"qrcode": "1.5.4",
"vue": "catalog:",
"vue-router": "catalog:"
},
+23
View File
@@ -0,0 +1,23 @@
export const NETWORK_QR_TYPE = "vnt-network";
export const NETWORK_QR_VERSION = 1;
export const buildNetworkQrPayload = (form) => {
const networkCode = String(form.network_code || "").trim();
const servers = (form.server || []).map((server) => String(server).trim()).filter(Boolean);
const mtu = Number(form.mtu || 1380);
if (!networkCode) throw new Error("组网编号不能为空");
if (servers.length === 0) throw new Error("服务器地址不能为空");
if (!Number.isInteger(mtu) || mtu < 576 || mtu > 9000) {
throw new Error("MTU 必须在 576-9000 之间");
}
return {
type: NETWORK_QR_TYPE,
version: NETWORK_QR_VERSION,
network_code: networkCode,
server: servers,
mtu,
password: String(form.password || ""),
};
};
+1 -1
View File
@@ -19,7 +19,7 @@ const message = ref("");
const downloaded = ref(0);
const contentLength = ref(0);
const currentVersion = computed(() => app.version || "2.0.1");
const currentVersion = computed(() => app.version || "2.0.2");
const progress = computed(() => {
if (!contentLength.value) return 0;
return Math.min(100, Math.round((downloaded.value / contentLength.value) * 100));
+77 -4
View File
@@ -1,16 +1,25 @@
<script setup>
import { ref, onMounted } from "vue";
import QRCode from "qrcode";
import { useAppStore } from "../stores/app";
import { useUiStore } from "../stores/ui";
import { deleteConfig } from "../api";
import { deleteConfig, getConfig } from "../api";
import EmptyState from "../components/EmptyState.vue";
import AppModal from "../components/AppModal.vue";
import ConfigEditor from "./ConfigEditor.vue";
import { parseTomlToForm } from "../utils/toml";
import { buildNetworkQrPayload } from "../utils/networkQr";
const app = useAppStore();
const ui = useUiStore();
const showEditor = ref(false);
const editorFileName = ref(null);
const showQr = ref(false);
const qrLoading = ref(false);
const qrImage = ref("");
const qrConfig = ref(null);
const qrError = ref("");
// 配置对应的实例运行状态(无实例返回 null)
const instStatus = (fileName) => {
@@ -34,6 +43,29 @@ const onSaved = () => {
app.fetchConfigList();
};
const openQr = async (fileName) => {
showQr.value = true;
qrLoading.value = true;
qrImage.value = "";
qrConfig.value = null;
qrError.value = "";
try {
const form = parseTomlToForm(await getConfig(fileName));
const payload = buildNetworkQrPayload(form);
qrConfig.value = payload;
qrImage.value = await QRCode.toDataURL(JSON.stringify(payload), {
errorCorrectionLevel: "M",
width: 360,
margin: 2,
color: { dark: "#020617", light: "#ffffff" },
});
} catch (e) {
qrError.value = e.message || "二维码生成失败";
} finally {
qrLoading.value = false;
}
};
const handleDelete = async (fileName) => {
const ok = await ui.confirm({
title: "删除配置",
@@ -79,19 +111,30 @@ onMounted(() => app.fetchConfigList());
class="card group relative flex min-h-[140px] cursor-pointer flex-col justify-between overflow-hidden"
@click="openEditor(cfg.file_name)"
>
<button
class="absolute right-3 top-3 z-10 rounded-lg p-1.5 text-slate-400 transition hover:bg-indigo-50 hover:text-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:hover:bg-indigo-500/10 dark:hover:text-indigo-400"
type="button"
title="显示加入网络二维码"
aria-label="显示加入网络二维码"
@click.stop="openQr(cfg.file_name)"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h7v7H3V3zm11 0h7v7h-7V3zM3 14h7v7H3v-7zm12 0h2m4 0v2m-6 2h2v3m4-3v3h-2" />
</svg>
</button>
<div
v-if="instStatus(cfg.file_name) === 'running'"
class="absolute right-0 top-0 rounded-bl bg-green-500 px-2 py-1 text-xs text-white"
class="absolute right-12 top-3 rounded bg-green-500 px-2 py-1 text-xs text-white"
>
运行中
</div>
<div
v-else-if="instStatus(cfg.file_name) === 'starting'"
class="absolute right-0 top-0 rounded-bl bg-indigo-500 px-2 py-1 text-xs text-white"
class="absolute right-12 top-3 rounded bg-indigo-500 px-2 py-1 text-xs text-white"
>
启动中
</div>
<div class="flex items-start">
<div class="flex items-start pr-28">
<div class="mr-3 mt-1 rounded-lg bg-indigo-50 p-2 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-400">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -131,6 +174,36 @@ onMounted(() => app.fetchConfigList());
@close="showEditor = false"
@saved="onSaved"
/>
<AppModal :show="showQr" panel-class="w-full max-w-md" @close="showQr = false">
<template #header>
<div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white">扫码加入网络</h2>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">使用 VNT 安卓客户端扫描</p>
</div>
<button class="btn-ghost btn-sm" type="button" @click="showQr = false">关闭</button>
</template>
<template #body>
<div class="p-6">
<div v-if="qrLoading" class="py-20 text-center text-sm muted">正在生成二维码</div>
<div v-else-if="qrError" class="rounded-lg bg-red-50 p-4 text-sm text-red-600 dark:bg-red-500/10 dark:text-red-400">
{{ qrError }}
</div>
<div v-else-if="qrConfig" class="space-y-5">
<div class="mx-auto w-fit rounded-xl border border-slate-200 bg-white p-3 shadow-sm">
<img :src="qrImage" class="h-auto w-full max-w-[320px]" alt="VNT 加入网络二维码" />
</div>
<dl class="space-y-2 rounded-lg bg-slate-50 p-4 text-sm dark:bg-slate-800/60">
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">组网编号</dt><dd class="min-w-0 break-all font-mono text-slate-900 dark:text-white">{{ qrConfig.network_code }}</dd></div>
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">服务器</dt><dd class="min-w-0 break-all font-mono text-slate-900 dark:text-white">{{ qrConfig.server.join(", ") }}</dd></div>
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">MTU</dt><dd class="font-mono text-slate-900 dark:text-white">{{ qrConfig.mtu }}</dd></div>
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">加密密码</dt><dd class="text-slate-900 dark:text-white">{{ qrConfig.password ? "已包含" : "未设置" }}</dd></div>
</dl>
<p class="text-xs leading-5 text-amber-600 dark:text-amber-400">二维码包含组网凭据请仅分享给可信设备</p>
</div>
</div>
</template>
</AppModal>
</div>
</template>