feat: 实现仪表盘页面及系统信息采集功能
新增系统信息采集后端服务,引入 sysinfo 依赖获取 CPU、内存、主机名、系统版本、架构和 IP 地址等数据;新增系统相关 API 接口和 TypeScript 类型定义;重构仪表盘页面,整合服务器/隧道数量统计、系统信息展示、资源占用监控和 Frpc 运行日志功能;新增中英文国际化文案支持,完善前端交互逻辑。
This commit is contained in:
Generated
+46
@@ -1254,6 +1254,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-helper",
|
||||
@@ -2504,6 +2505,15 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -2670,6 +2680,16 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
@@ -2681,6 +2701,17 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-open-directory"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
@@ -4210,6 +4241,21 @@ dependencies = [
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.39.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"objc2-open-directory",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
|
||||
@@ -35,6 +35,7 @@ sqlx = { version = "0.9.0", features = [
|
||||
"chrono",
|
||||
] }
|
||||
chrono = { version = "0.4.45", features = ["serde"] }
|
||||
sysinfo = "0.39"
|
||||
tauri-plugin-dialog = "2.7.2"
|
||||
tauri-plugin-updater = "2.10.1"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod tunnel;
|
||||
pub mod server;
|
||||
pub mod server;
|
||||
pub mod system;
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use sysinfo::{Networks, System};
|
||||
use tauri::State;
|
||||
use tauri_helper::auto_collect_command;
|
||||
|
||||
use crate::models::{ApiResponse, SystemDetails, SystemInfo};
|
||||
use crate::services::system::SystemStats;
|
||||
|
||||
/// 获取系统资源占用(CPU / 内存百分比)
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn get_system_info(
|
||||
state: State<'_, SystemStats>,
|
||||
) -> Result<ApiResponse<SystemInfo>, String> {
|
||||
let (cpu, memory) = state.snapshot();
|
||||
Ok(ApiResponse::success(SystemInfo { cpu, memory }))
|
||||
}
|
||||
|
||||
/// 获取系统信息(主机名、系统版本、架构、IP 地址)
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn get_system_details() -> Result<ApiResponse<SystemDetails>, String> {
|
||||
Ok(ApiResponse::success(collect_details()))
|
||||
}
|
||||
|
||||
fn collect_details() -> SystemDetails {
|
||||
let hostname = System::host_name().unwrap_or_else(|| "-".to_string());
|
||||
let os = System::long_os_version()
|
||||
.or_else(System::os_version)
|
||||
.or_else(System::name)
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let arch = System::cpu_arch();
|
||||
|
||||
// 采集本机 IPv4 地址(过滤回环地址),并去重
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let mut ips: Vec<String> = Vec::new();
|
||||
for data in networks.list().values() {
|
||||
for ip_net in data.ip_networks() {
|
||||
if let IpAddr::V4(ipv4) = ip_net.addr {
|
||||
if !ipv4.is_loopback() {
|
||||
ips.push(ipv4.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ips.sort();
|
||||
ips.dedup();
|
||||
|
||||
SystemDetails {
|
||||
hostname,
|
||||
os,
|
||||
arch,
|
||||
ips,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn details_are_collected() {
|
||||
let d = collect_details();
|
||||
println!(
|
||||
"hostname={}, os={}, arch={}, ips={:?}",
|
||||
d.hostname, d.os, d.arch, d.ips
|
||||
);
|
||||
assert!(!d.hostname.is_empty());
|
||||
assert!(!d.arch.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ use crate::commands::server::{
|
||||
create_server, delete_server, get_server_list, set_server_enabled, set_server_pinned,
|
||||
update_server,
|
||||
};
|
||||
use crate::commands::system::{get_system_details, get_system_info};
|
||||
use crate::commands::tunnel::{
|
||||
create_tunnel, delete_tunnel, get_tunnel_list, set_tunnel_enabled, set_tunnel_pinned,
|
||||
update_tunnel,
|
||||
};
|
||||
use crate::services::db::init_pool;
|
||||
use crate::services::system::SystemStats;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -30,6 +32,11 @@ pub fn run() {
|
||||
});
|
||||
app.manage(pool);
|
||||
|
||||
// 系统资源占用(CPU / 内存)后台采集
|
||||
let stats = SystemStats::default();
|
||||
stats.start();
|
||||
app.manage(stats);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
|
||||
+23
-2
@@ -157,8 +157,7 @@ pub struct Tunnel {
|
||||
|
||||
/// 新增 / 编辑隧道的提交数据
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TunnelPayload {
|
||||
/// 编辑时携带,新增时为空
|
||||
pub struct TunnelPayload { /// 编辑时携带,新增时为空
|
||||
pub id: Option<i64>,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
@@ -187,3 +186,25 @@ pub struct TunnelPayload {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// 系统资源占用(百分比,0 - 100)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SystemInfo {
|
||||
/// CPU 占用百分比
|
||||
pub cpu: u32,
|
||||
/// 内存占用百分比
|
||||
pub memory: u32,
|
||||
}
|
||||
|
||||
/// 系统信息(主机名、系统版本、架构、IP 地址)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SystemDetails {
|
||||
/// 主机名
|
||||
pub hostname: String,
|
||||
/// 操作系统版本
|
||||
pub os: String,
|
||||
/// 系统架构
|
||||
pub arch: String,
|
||||
/// 本机 IPv4 地址列表(不含回环地址)
|
||||
pub ips: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod db;
|
||||
pub mod db;
|
||||
pub mod system;
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
|
||||
|
||||
/// 系统资源占用(CPU / 内存百分比)。
|
||||
///
|
||||
/// 通过后台线程定时刷新并缓存,命令调用时直接读取缓存值,
|
||||
/// 避免每次请求都阻塞等待 CPU 采样间隔。
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SystemStats {
|
||||
cpu: Arc<AtomicU32>,
|
||||
memory: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
impl SystemStats {
|
||||
/// 启动后台刷新线程(每秒刷新一次 CPU / 内存占用)
|
||||
pub fn start(&self) {
|
||||
let cpu = self.cpu.clone();
|
||||
let memory = self.memory.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
// 仅刷新 CPU 使用率与内存,避免枚举进程导致首次采样过慢
|
||||
let mut sys = System::new_with_specifics(
|
||||
RefreshKind::nothing()
|
||||
.with_cpu(CpuRefreshKind::nothing().with_cpu_usage())
|
||||
.with_memory(MemoryRefreshKind::nothing().with_ram()),
|
||||
);
|
||||
// 首次刷新后间隔一小段时间再采样,保证 CPU 使用率准确
|
||||
sys.refresh_cpu_usage();
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
|
||||
loop {
|
||||
sys.refresh_cpu_usage();
|
||||
sys.refresh_memory();
|
||||
|
||||
cpu.store(sys.global_cpu_usage().round() as u32, Ordering::Relaxed);
|
||||
memory.store(
|
||||
usage_percent(sys.used_memory(), sys.total_memory()),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 读取当前缓存的占用百分比
|
||||
pub fn snapshot(&self) -> (u32, u32) {
|
||||
(
|
||||
self.cpu.load(Ordering::Relaxed),
|
||||
self.memory.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算百分比(0 - 100)
|
||||
fn usage_percent(used: u64, total: u64) -> u32 {
|
||||
if total == 0 {
|
||||
return 0;
|
||||
}
|
||||
((used as f64 / total as f64) * 100.0).round() as u32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_returns_real_percentages() {
|
||||
let stats = SystemStats::default();
|
||||
stats.start();
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
let (cpu, memory) = stats.snapshot();
|
||||
println!("real cpu = {cpu}%, memory = {memory}%");
|
||||
assert!(cpu <= 100, "cpu out of range: {cpu}");
|
||||
assert!(memory <= 100, "memory out of range: {memory}");
|
||||
assert!(memory > 0, "memory should be greater than 0");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user