Compare commits

..

2 Commits

Author SHA1 Message Date
Chaos
514c025caf feat(network): 添加网络接口信息获取功能
- 新增 network-interface 依赖用于获取系统网络接口- 实现 Rust 端 get_network_interfaces 命令- 在 Svelte 前端调用 Tauri 命令并展示网络接口信息
- 添加接口信息表格展示及错误处理
- 更新布局结构移除多余注释和空行
2025-11-10 16:50:26 +08:00
Chaos
cacb25a2ca feat(layout): 添加 SVG 图标系统并更新侧边栏导航
- 引入 Sprite 组件统一管理 SVG 图标
- 创建 Icon 组件支持动态加载图标- 定义 icon-ids 类型确保图标引用安全- 更新 Sidebar 使用新图标组件替换 emoji
- 添加 HomeIcon 和 network 图标资源- 调整侧边栏样式和宽度
- 修复 Header 按钮 title 属性替代 aria-label
- 新增 IP 工具页面路由
- 添加全局阴影变量 --main-border-shadow
2025-11-10 08:31:56 +08:00
20 changed files with 305 additions and 25 deletions

9
package-lock.json generated
View File

@@ -867,6 +867,7 @@
"integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==", "integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@sveltejs/acorn-typescript": "^1.0.5", "@sveltejs/acorn-typescript": "^1.0.5",
@@ -906,6 +907,7 @@
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"debug": "^4.4.1", "debug": "^4.4.1",
@@ -1467,6 +1469,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2055,6 +2058,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2097,6 +2101,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -2113,6 +2118,7 @@
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"prettier": "^3.0.0", "prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
@@ -2304,6 +2310,7 @@
"integrity": "sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==", "integrity": "sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -2402,6 +2409,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2416,6 +2424,7 @@
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",

8
src-tauri/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
src-tauri/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/src-tauri.iml" filepath="$PROJECT_DIR$/.idea/src-tauri.iml" />
</modules>
</component>
</project>

11
src-tauri/.idea/src-tauri.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
src-tauri/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

13
src-tauri/Cargo.lock generated
View File

@@ -80,6 +80,7 @@ name = "app"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"log", "log",
"network-interface",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@@ -1904,6 +1905,18 @@ dependencies = [
"jni-sys", "jni-sys",
] ]
[[package]]
name = "network-interface"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745"
dependencies = [
"cc",
"libc",
"thiserror 2.0.17",
"winapi",
]
[[package]] [[package]]
name = "new_debug_unreachable" name = "new_debug_unreachable"
version = "1.0.6" version = "1.0.6"

View File

@@ -19,7 +19,8 @@ tauri-build = { version = "2.5.1", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0.228" }
log = "0.4" log = "0.4"
tauri = { version = "2.9.2", features = [] } tauri = { version = "2.9.2", features = [] }
tauri-plugin-log = "2" tauri-plugin-log = "2"
network-interface = "2.0.3"

View File

@@ -1,3 +1,5 @@
mod network;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
@@ -11,6 +13,9 @@ pub fn run() {
} }
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![
network::get_network_interfaces
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -1,6 +1,10 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
app_lib::run(); app_lib::run();
} }

37
src-tauri/src/network.rs Normal file
View File

@@ -0,0 +1,37 @@
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct InterfaceInfo {
pub name: String,
pub addr: String, // IP 地址
pub mac: Option<String>, // MAC 地址,可能不存在
}
fn map_interface_to_info(interface: &NetworkInterface) -> InterfaceInfo {
let mac_address = interface.mac_addr.as_ref().map(|mac| mac.to_string());
let ip_addr = interface.addr.iter().find(|addr| addr.ip().is_ipv4())
.map(|addr| addr.ip().to_string())
.unwrap_or_else(|| "N/A".to_string());
InterfaceInfo {
name: interface.name.clone(),
addr: ip_addr,
mac: mac_address,
}
}
#[tauri::command]
pub async fn get_network_interfaces() -> Result<Vec<InterfaceInfo>, String> {
match NetworkInterface::show() {
Ok(interfaces) => {
let info_list: Vec<InterfaceInfo> = interfaces.into_iter()
.map(|interface| map_interface_to_info(&interface))
.collect();
Ok(info_list)
}
Err(e) => Err(format!("无法获取网络接口信息: {}", e)),
}
}

View File

@@ -2,6 +2,7 @@
:root{ :root{
--main-bg-color: #1f1f1f; --main-bg-color: #1f1f1f;
--main-border-shadow:1px 0 3px rgba(255, 255, 255, 0.1);
} }
body { body {

View File

@@ -1,3 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" > <svg xmlns="http://www.w3.org/2000/svg" style="display:none" >
<symbol id="home" viewBox="0 0 20 20">
<g fill="none">
<path fill="url(#home-SVGDx0DLj5H)" d="M13.18 2H10.5C8.768 7.023 6.823 12.62 6.823 18h6.483a1.75 1.75 0 0 0 1.485-.825l3.998-6.42a1.43 1.43 0 0 0 0-1.51L14.98 3.13A2 2 0 0 0 13.18 2"/>
<path fill="url(#home-SVGpR69Vbpe)" fill-opacity="0.5" d="M13.18 2H10.5C8.768 7.023 6.823 12.62 6.823 18h6.483a1.75 1.75 0 0 0 1.485-.825l3.998-6.42a1.43 1.43 0 0 0 0-1.51L14.98 3.13A2 2 0 0 0 13.18 2"/>
<path fill="url(#home-SVGFEeptdiY)" d="M13.25 2.001H6.69c-.601 0-1.16.308-1.48.816l-3.942 6.25a1.75 1.75 0 0 0 0 1.867L5.13 17.06c.354.56.96.91 1.619.938l.067.002h.006a2 2 0 0 0 1.969-1.662l.003-.002L11.18 3.953a2 2 0 0 1 2.069-1.952"/>
<path fill="url(#home-SVGIBIxBcZq)" fill-opacity="0.4" d="M13.25 2.001H6.69c-.601 0-1.16.308-1.48.816l-3.942 6.25a1.75 1.75 0 0 0 0 1.867L5.13 17.06c.354.56.96.91 1.619.938l.067.002h.006a2 2 0 0 0 1.969-1.662l.003-.002L11.18 3.953a2 2 0 0 1 2.069-1.952"/>
<defs>
<radialGradient id="home-SVGDx0DLj5H" cx="0" cy="0" r="1" gradientTransform="rotate(-87.881 17.698 4.836)scale(23.3302 18.6978)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffc470"/><stop offset=".251" stop-color="#ff835c"/><stop offset=".584" stop-color="#f24a9d"/><stop offset=".871" stop-color="#b339f0"/><stop offset="1" stop-color="#c354ff"/></radialGradient>
<radialGradient id="home-SVGpR69Vbpe" cx="0" cy="0" r="1" gradientTransform="matrix(-9.9932 -9.83058 9.94854 -10.1131 11.777 16.154)" gradientUnits="userSpaceOnUse"><stop offset=".709" stop-color="#ffb357" stop-opacity="0"/><stop offset=".942" stop-color="#ffb357"/></radialGradient>
<radialGradient id="home-SVGFEeptdiY" cx="0" cy="0" r="1" gradientTransform="rotate(-160.247 10.243 6.665)scale(22.9945 19.4416)" gradientUnits="userSpaceOnUse"><stop offset=".222" stop-color="#4e46e2"/><stop offset=".578" stop-color="#625df6"/><stop offset=".955" stop-color="#e37dff"/></radialGradient>
<linearGradient id="home-SVGIBIxBcZq" x1="4.823" x2="10.254" y1="8.629" y2="9.914" gradientUnits="userSpaceOnUse"><stop stop-color="#7563f7" stop-opacity="0"/><stop offset=".986" stop-color="#4916ae"/></linearGradient>
</defs>
</g>
</symbol>
<symbol id="network" viewBox="0 0 24 24"><path fill="currentColor" d="M7 7H5V6H4V4h1V3h2v1h1v2H7zm-2 4h1v3H5v1H2v-1H1v-3h1v-1h3zm4-3h1v1H9zm6 7h1v1h-1zm2-7h1v1h-1zM8 7h1v1H8zm-1 5h1v1H7z"/><path fill="currentColor" d="M16 14v-3h-1v-1h-1V9h-3v1h-1v1H9v3h1v1h1v1h3v-1h1v-1zm-5 0v-3h3v3zm5-5h1v1h-1zm0 7h1v1h-1zm5 2h1v3h-1v1h-3v-1h-1v-3h1v-1h3zm1-13v2h-1v1h-2V7h-1V5h1V4h2v1z"/></symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 70 B

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import type { IconId } from '$lib/types/icon-ids';
export let id: IconId;
export let size: number | string ;
export let className: string = '';
$: dimensions = typeof size === 'number' ? `${size}px` : size;
</script>
<svg {...$$restProps}
role="img"
class={className}
aria-hidden="true"
width={dimensions?dimensions:24}
height={dimensions?dimensions:24}
>
<use href={`#${id}`} />
</svg>
<style>
.app-icon {
/* 确保图标与文本基线对齐 */
vertical-align: middle;
/* 防止用户选择图标,提高用户体验 */
user-select: none;
/* 默认显示为行内块级元素 */
display: inline-block;
/* 确保它能响应 CSS 动画 */
transition: color 0.2s, transform 0.2s;
}
</style>

View File

@@ -0,0 +1,23 @@
<script>
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" {...$$restProps}>
<g fill="none">
<path fill="url(#SVGkm4eqcHj)" d="M6 9h4v5H6z" />
<path fill="url(#SVG4sqiUdhg)" d="M8.687 2.273a1 1 0 0 0-1.374 0l-4.844 4.58A1.5 1.5 0 0 0 2 7.943v4.569a1.5 1.5 0 0 0 1.5 1.5h3v-4a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v4h3a1.5 1.5 0 0 0 1.5-1.5v-4.57a1.5 1.5 0 0 0-.47-1.09z" />
<path fill="url(#SVGbhYexcuE)" fill-rule="evenodd" d="m8.004 2.636l5.731 5.41a.75.75 0 1 0 1.03-1.091L8.86 1.382a1.25 1.25 0 0 0-1.724.007L1.23 7.059a.75.75 0 0 0 1.038 1.082z" clip-rule="evenodd" />
<defs>
<linearGradient id="SVGkm4eqcHj" x1="8" x2="4.796" y1="9" y2="14.698" gradientUnits="userSpaceOnUse">
<stop stop-color="#944600" />
<stop offset="1" stop-color="#cd8e02" />
</linearGradient>
<linearGradient id="SVG4sqiUdhg" x1="3.145" x2="14.93" y1="1.413" y2="10.981" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffd394" />
<stop offset="1" stop-color="#ffb357" />
</linearGradient>
<linearGradient id="SVGbhYexcuE" x1="10.262" x2="6.945" y1="-.696" y2="7.895" gradientUnits="userSpaceOnUse">
<stop stop-color="#ff921f" />
<stop offset="1" stop-color="#eb4824" />
</linearGradient>
</defs>
</g>
</svg>

View File

@@ -11,22 +11,21 @@
</script> </script>
<header class="custom-titlebar" data-tauri-drag-region> <header class="custom-titlebar" data-tauri-drag-region>
<div class="app-title" data-tauri-drag-region>
</div> <div class="app-title" data-tauri-drag-region></div>
<div class="window-controls"> <div class="window-controls">
<button type="button" aria-label="最小化窗口" data-tauri-drag-region="false" class="h-btn btn-minimize" on:click={minimize}></button> <button type="button" title="最小化窗口" data-tauri-drag-region="false" class="h-btn btn-minimize" on:click={minimize}></button>
<button <button
type="button" type="button"
data-tauri-drag-region="false" data-tauri-drag-region="false"
class="h-btn btn-max" class="h-btn btn-max"
aria-label="最大化窗口" title="最大化窗口"
on:click={maximize}></button> on:click={maximize}></button>
<button type="button" <button type="button"
data-tauri-drag-region="false" data-tauri-drag-region="false"
class="h-btn btn-close" class="h-btn btn-close"
aria-label="关闭窗口" title="关闭应用"
on:click={closeWindow}></button> on:click={closeWindow}></button>
</div> </div>
</header> </header>

View File

@@ -4,26 +4,34 @@
// 模拟导航项数据 // 模拟导航项数据
// 确保对象字面量语法正确无误 // 确保对象字面量语法正确无误
const navItems = [ import { goto } from '$app/navigation';
{ icon: '🏠', label: '主页', path: '/' }, import Icon from '$lib/components/Icon.svelte';
{ icon: '⭐', label: '收藏', path: '/favorites' }, import type { IconId } from '$lib/types/icon-ids';
{ icon: '💬', label: '聊天', path: '/chat' }, import HomeIcon from '$lib/components/icon/HomeIcon.svelte';
{ icon: '⚙️', label: '设置', path: '/settings' },
const navItems:[{icon: IconId, label: string, path: string}] = [
{ icon: 'network', label: 'IP工具', path: '/iptools' },
]; ];
let activePath: string = '/'; // 明确声明类型 let activePath: string = '/'; // 明确声明类型
function navigate(path: string) { function navigate(path: string) {
activePath = path; activePath = path;
// goto(path); // 实际应用中使用 SvelteKit 的导航 goto(path);
console.log(`导航到: ${path}`); console.log(`导航到: ${path}`);
} }
</script> </script>
<nav class="sidebar"> <nav class="sidebar">
<!-- 顶部图标/Logo --> <!-- 顶部图标/Logo -->
<div class="sidebar-logo"> <div class="sidebar-logo pt-2 pb-2">
<span role="img" aria-label="App Logo">⚛️</span> <button type="button"
class="cursor-pointer"
title="回到主页"
on:click={() => navigate('/')}>
<HomeIcon width="24" height="24" />
</button>
</div> </div>
<!-- 中间导航图标 --> <!-- 中间导航图标 -->
@@ -35,7 +43,7 @@
on:click={() => navigate(item.path)} on:click={() => navigate(item.path)}
title={item.label} title={item.label}
> >
{item.icon} <Icon id={item.icon} className="hover:text-neutral-400 transition-all duration-1000" size="24" />
</button> </button>
{/each} {/each}
</div> </div>
@@ -51,7 +59,7 @@
/* 侧边栏的基础样式 */ /* 侧边栏的基础样式 */
.sidebar { .sidebar {
/* 根据图片,侧边栏宽度较窄,可能在 60px 左右 */ /* 根据图片,侧边栏宽度较窄,可能在 60px 左右 */
width: 25px; width: 35px;
min-width: 35px; /* 确保它不会收缩 */ min-width: 35px; /* 确保它不会收缩 */
height: 100%; height: 100%;
background-color: var(--main-bg-color); /* 比主背景稍亮 */ background-color: var(--main-bg-color); /* 比主背景稍亮 */
@@ -59,14 +67,12 @@
flex-direction: column; /* 垂直排列内容 */ flex-direction: column; /* 垂直排列内容 */
align-items: center; align-items: center;
padding: 10px 0; padding: 10px 0;
box-shadow: 1px 0 3px rgba(255, 255, 255, 0.1); box-shadow: var(--main-border-shadow);
user-select: none; /* 阻止文本选择 */ user-select: none; /* 阻止文本选择 */
} }
/* Logo/顶部区域 */ /* Logo/顶部区域 */
.sidebar-logo { .sidebar-logo {
font-size: 1rem;
margin-bottom: 20px;
cursor: pointer; cursor: pointer;
} }

View File

@@ -0,0 +1,16 @@
<script>
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" >
<symbol id="network" viewBox="0 0 24 24">
<g fill="none">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 7.96c2.59-.125 4.379.274 4.625 1.193c.429 1.6-3.98 4.172-9.849 5.745c-5.868 1.572-10.972 1.55-11.401-.051c-.254-.948 1.188-2.236 3.625-3.455" />
<path fill="currentColor" fill-rule="evenodd" d="M4 12a8 8 0 1 1 15.985.491c-1.653.879-3.904 1.754-6.467 2.44c-2.874.77-5.526 1.14-7.478 1.131a12 12 0 0 1-.956-.039A7.96 7.96 0 0 1 4 12m2.766 6.05a8.003 8.003 0 0 0 12.658-3.065c-1.561.697-3.4 1.346-5.389 1.879c-2.668.715-5.208 1.115-7.269 1.186" clip-rule="evenodd" />
</g>
</symbol>
</svg>

View File

@@ -0,0 +1,3 @@
export type IconId =
| 'home'
| 'network' ;

View File

@@ -3,23 +3,20 @@
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
import Header from '$lib/components/layout/Header.svelte'; import Header from '$lib/components/layout/Header.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import Sprite from '$lib/components/ui/Sprite.svelte';
let { children } = $props(); let { children } = $props();
</script> </script>
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
<Sprite />
</svelte:head> </svelte:head>
<Header/> <Header/>
<div class="app-container"> <div class="app-container">
<!-- 1. 侧边栏: 固定宽度,全高 -->
<Sidebar /> <Sidebar />
<!-- 2. 主内容区域: 占据剩余所有空间 (flex-1) -->
<main class="main-content"> <main class="main-content">
<!-- 渲染子路由的内容 (+page.svelte 或下级 +layout.svelte)
使用标准的 <slot /> 替换了 {@render children()} -->
{@render children()} {@render children()}
</main> </main>
</div> </div>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte';
// 定义与 Rust 端 InterfaceInfo 匹配的 TypeScript 类型
interface InterfaceInfo {
name: string;
addr: string;
mac?: string;
}
let interfaces: InterfaceInfo[] = [];
let loading: boolean = true;
let error: string | null = null;
onMount(async () => {
try {
// 调用 Rust 后端命令
interfaces = await invoke<InterfaceInfo[]>("get_network_interfaces");
console.log("Network interfaces:", interfaces);
} catch (err) {
console.error("Failed to get network interfaces:", err);
// 错误处理,将错误信息展示给用户
error = (err as string) || "未知错误";
} finally {
loading = false;
}
});
</script>
<div class="network-info">
<h2>🌐 网络接口信息</h2>
{#if loading}
<p>正在加载...</p>
{:else if error}
<p class="error-message">错误: {error}</p>
{:else}
<table>
<thead>
<tr>
<th>名称</th>
<th>IP 地址 (IPv4)</th>
<th>MAC 地址</th>
</tr>
</thead>
<tbody>
{#each interfaces as iface (iface.name)}
<tr>
<td><strong>{iface.name}</strong></td>
<td>{iface.addr}</td>
<td>{iface.mac || 'N/A'}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<style>
/* 简单的 Svelte 样式 */
table {
width: 100%;
border-collapse: collapse;
margin-top: 1em;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: var(--main-bg-color);
}
.error-message {
color: red;
font-weight: bold;
}
</style>