diff --git a/docs/Frpilot开发提示词.md b/docs/Frpilot开发提示词.md index 749eb51..a226a4b 100644 --- a/docs/Frpilot开发提示词.md +++ b/docs/Frpilot开发提示词.md @@ -67,4 +67,20 @@ Layout布局content区域的磨砂玻璃卡片圆角样式我要改动一下: - 页面展示已经配置的隧道列表,列表隧道以卡片形式展示,卡片有开关可以直接操作是否置顶和是否启用,隧道可同时启用多个,另外还需要有编辑和删除,新增和编辑使用同一个表单 - 隧道信息在Rust端使用SQLite存储,Rust已经加入相关的sqlx依赖等,后端操作返回前端务必使用ApiResponse统一返回格式,前端也需要做好对应的类型匹配 -开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件、auto-import和components依赖可以简化引入,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 \ No newline at end of file +开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件、auto-import和components依赖可以简化引入,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 + +#### 3.3 仪表盘页面开发 + +按照我给的图片布局开发仪表盘页面 + +- 三个卡片分别展示服务器数量、隧道数量、CPU/内存/运行状态,每个卡片都需要圆角磨砂玻璃效果 +- 三个卡片下方是快速操作,可以选择已下载的Frpc版本,右侧按钮一键启动或停止,Frpc下载功能暂未实现,先用模拟数据展示 +- 快速操作下方是Frpc运行实时日志展示,也先使用模拟数据展示 + +开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件、auto-import和components依赖可以简化引入,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 + +##### 3.3.1 仪表盘页面调整 + +- 三个卡片当中的服务器数量和隧道数量合并到一个卡片里面,这一个卡片里面上方展示服务器数量,下方展示隧道数量 +- 原本的隧道数量卡片改为展示当前运行系统主机名、系统架构、IP地址等,通过Rust端sysinfo获取 +- 其余卡片保持不动 \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9b83a0e..796c3c8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2b1f476..f77ebca 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 988f16c..c4898f6 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod tunnel; -pub mod server; \ No newline at end of file +pub mod server; +pub mod system; \ No newline at end of file diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs new file mode 100644 index 0000000..a7fc7d9 --- /dev/null +++ b/src-tauri/src/commands/system.rs @@ -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, 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, 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 = 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()); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f44c0a6..e63b149 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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()) diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index bb78bb5..89a0255 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -157,8 +157,7 @@ pub struct Tunnel { /// 新增 / 编辑隧道的提交数据 #[derive(Debug, Serialize, Deserialize)] -pub struct TunnelPayload { - /// 编辑时携带,新增时为空 +pub struct TunnelPayload { /// 编辑时携带,新增时为空 pub id: Option, 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, +} + diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 8c5eabd..17b0a41 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1 +1,2 @@ -pub mod db; \ No newline at end of file +pub mod db; +pub mod system; \ No newline at end of file diff --git a/src-tauri/src/services/system.rs b/src-tauri/src/services/system.rs new file mode 100644 index 0000000..29da380 --- /dev/null +++ b/src-tauri/src/services/system.rs @@ -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, + memory: Arc, +} + +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"); + } +} diff --git a/src/api/system.ts b/src/api/system.ts new file mode 100644 index 0000000..e3f6f3b --- /dev/null +++ b/src/api/system.ts @@ -0,0 +1,27 @@ +import { invoke } from "@tauri-apps/api/core" +import type { ApiResponse } from "@/types/api" +import type { SystemDetails, SystemInfo } from "@/types/system" + +/** + * 调用后端命令并统一解包 ApiResponse。 + * + * 后端系统相关命令均返回 `ApiResponse`,业务失败时 + * `success === false`,此处统一抛出异常,交由调用方处理。 + */ +async function call(command: string, args?: Record): Promise { + const res = await invoke>(command, args) + if (!res?.success) { + throw new Error(res?.msg || "操作失败") + } + return res.data as T +} + +/** 获取系统资源占用(CPU / 内存百分比) */ +export function getSystemInfo() { + return call("get_system_info") +} + +/** 获取系统信息(主机名、系统版本、架构、IP 地址) */ +export function getSystemDetails() { + return call("get_system_details") +} diff --git a/src/composables/useSystemStats.ts b/src/composables/useSystemStats.ts index bff73fe..1c82be0 100644 --- a/src/composables/useSystemStats.ts +++ b/src/composables/useSystemStats.ts @@ -1,16 +1,11 @@ -import { invoke } from "@tauri-apps/api/core" +import { getSystemInfo } from "@/api/system" import { isTauri } from "@/utils/window" -interface SystemInfo { - cpu: number - memory: number -} - /** * 系统资源占用(CPU / 内存 百分比)。 * - * 优先调用后端命令 `get_system_info`(返回 { cpu, memory }), - * 后端尚未实现时回退到模拟数据,保证界面可用。 + * 调用后端命令 `get_system_info`(返回 `ApiResponse<{ cpu, memory }>`), + * 在非 Tauri 环境或后端不可用时回退到模拟数据,保证界面可用。 */ export function useSystemStats(interval = 2000) { const cpu = ref(0) @@ -31,15 +26,13 @@ export function useSystemStats(interval = 2000) { async function refresh() { if (isTauri()) { try { - const res = await invoke("get_system_info") - if (res) { - cpu.value = Math.round(res.cpu) - memory.value = Math.round(res.memory) - isMock.value = false - return - } + const info = await getSystemInfo() + cpu.value = Math.round(info.cpu) + memory.value = Math.round(info.memory) + isMock.value = false + return } catch { - // 后端命令未实现,回退到模拟数据 + // 后端命令不可用,回退到模拟数据 } } mock() diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 2bdb3e2..f8b73c1 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -33,6 +33,24 @@ "statusError": "Error", "statusStopped": "Stopped" }, + "dashboard": { + "serverCount": "Servers", + "tunnelCount": "Tunnels", + "hostname": "Hostname", + "os": "OS", + "arch": "Architecture", + "ip": "IP address", + "frpcVersion": "Frpc version", + "start": "One-click start", + "stop": "Stop", + "logs": "Live logs", + "clear": "Clear", + "mock": "Mock data", + "logEmpty": "No logs yet. Live logs will appear here once Frpc starts.", + "logReady": "Frpilot is ready. Waiting for Frpc to start.", + "logStarted": "Frpc {version} started", + "logStopped": "Frpc stopped" + }, "settings": { "title": "Settings", "placeholder": "The settings page is under construction.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 317ddf2..50b8f07 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -33,6 +33,24 @@ "statusError": "运行异常", "statusStopped": "未启动" }, + "dashboard": { + "serverCount": "服务器数量", + "tunnelCount": "隧道数量", + "hostname": "主机名", + "os": "系统", + "arch": "系统架构", + "ip": "IP 地址", + "frpcVersion": "Frpc 版本", + "start": "一键启动", + "stop": "停止", + "logs": "实时日志", + "clear": "清空", + "mock": "模拟数据", + "logEmpty": "暂无日志,启动 Frpc 后将在此显示实时日志", + "logReady": "Frpilot 已就绪,等待启动 Frpc", + "logStarted": "已启动 Frpc {version}", + "logStopped": "已停止 Frpc" + }, "settings": { "title": "设置", "placeholder": "设置页面开发中,敬请期待。", diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 7df223c..2cc27cc 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -36,6 +36,7 @@ declare module 'vue' { ASelect: typeof import('ant-design-vue/es')['Select'] ASpace: typeof import('ant-design-vue/es')['Space'] ASpin: typeof import('ant-design-vue/es')['Spin'] + AStatistic: typeof import('ant-design-vue/es')['Statistic'] ASwitch: typeof import('ant-design-vue/es')['Switch'] ATag: typeof import('ant-design-vue/es')['Tag'] ATextarea: typeof import('ant-design-vue/es')['Textarea'] diff --git a/src/types/system.ts b/src/types/system.ts new file mode 100644 index 0000000..e40148b --- /dev/null +++ b/src/types/system.ts @@ -0,0 +1,23 @@ +/** + * 系统相关的类型定义,字段与 Rust 端 `models::SystemInfo` / `models::SystemDetails` 一一对应。 + */ + +/** 系统资源占用(百分比) */ +export interface SystemInfo { + /** CPU 占用百分比 */ + cpu: number + /** 内存占用百分比 */ + memory: number +} + +/** 系统信息(主机名、系统版本、架构、IP 地址) */ +export interface SystemDetails { + /** 主机名 */ + hostname: string + /** 操作系统版本 */ + os: string + /** 系统架构 */ + arch: string + /** 本机 IPv4 地址列表(不含回环地址) */ + ips: string[] +} diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 0de027e..124c4d9 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -1,19 +1,326 @@