feat: 添加前后端交互的示例代码
This commit is contained in:
parent
601b373593
commit
f8ec30acfc
12
package.json
12
package.json
@ -10,18 +10,20 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"antd": "^5.22.3",
|
"antd": "^5.22.3",
|
||||||
"react": "^18.2.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"sass": "^1.82.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.3.1"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1157
pnpm-lock.yaml
1157
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
12
src-tauri/Cargo.lock
generated
12
src-tauri/Cargo.lock
generated
@ -1763,6 +1763,16 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@ -3445,6 +3455,8 @@ dependencies = [
|
|||||||
name = "test-tauri"
|
name = "test-tauri"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"memmap",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
@ -23,4 +23,6 @@ tauri-plugin-shell = "2"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "2.0.4"
|
thiserror = "2.0.4"
|
||||||
|
bitflags = "2.6.0"
|
||||||
|
memmap = "0.7.0"
|
||||||
|
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
use thiserror;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
// 未打开文件映射
|
||||||
|
#[error("文件未打开!")]
|
||||||
|
NoFileOpened,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for AppError {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.to_string().as_ref())
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,74 @@
|
|||||||
use thiserror;
|
use crate::{app_error::AppError, services};
|
||||||
|
use serde::Serialize;
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[tauri::command]
|
||||||
enum AppError {
|
pub fn greet(name: &str) -> String {
|
||||||
#[error(transparent)]
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
Io(#[from] std::io::Error),
|
}
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PeNodeTreeData {
|
||||||
|
title: String,
|
||||||
|
key: String,
|
||||||
|
children: Vec<PeNodeTreeData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl serde::Serialize for AppError {
|
// TODO: 获取PE节点树的JSON数据
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
#[tauri::command]
|
||||||
where
|
pub fn command_get_file_pe_node_tree_data() -> Result<Vec<PeNodeTreeData>, AppError> {
|
||||||
S: serde::ser::Serializer,
|
// 1. 如果全局内存映射句柄为空,则返回错误
|
||||||
{
|
if unsafe { services::GLOBAL_MEMMAP_HANDLE.is_none() } {
|
||||||
serializer.serialize_str(self.to_string().as_ref())
|
return Err(AppError::NoFileOpened);
|
||||||
|
}
|
||||||
|
// 从文件路径中获取文件名
|
||||||
|
let file_name: &str = unsafe {
|
||||||
|
let file_path = services::GLOBAL_FILE_PATH.as_ref().unwrap();
|
||||||
|
let file_name = std::path::Path::new(file_path).file_name().unwrap();
|
||||||
|
file_name.to_str().unwrap()
|
||||||
|
};
|
||||||
|
let result = PeNodeTreeData {
|
||||||
|
title: format!("文件: {}", file_name),
|
||||||
|
key: "fileName".to_string(),
|
||||||
|
children: vec![
|
||||||
|
PeNodeTreeData {
|
||||||
|
title: "DOS 头部".to_string(),
|
||||||
|
key: "DOS Header".to_string(),
|
||||||
|
children: vec![], // 空数组表示没有子节点
|
||||||
|
},
|
||||||
|
PeNodeTreeData {
|
||||||
|
title: "NT 头部".to_string(),
|
||||||
|
key: "NT Headers".to_string(),
|
||||||
|
children: vec![
|
||||||
|
PeNodeTreeData {
|
||||||
|
title: "文件头部".to_string(),
|
||||||
|
key: "File Header".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
PeNodeTreeData {
|
||||||
|
title: "可选头部".to_string(),
|
||||||
|
key: "Optional Header".to_string(),
|
||||||
|
children: vec![PeNodeTreeData {
|
||||||
|
title: "数据目录".to_string(),
|
||||||
|
key: "Data Directories".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
PeNodeTreeData {
|
||||||
|
title: "节区头部".to_string(),
|
||||||
|
key: "Section Headers".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
Ok(vec![result])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令,打开文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn command_open_file(file_path: &str) -> Result<(), AppError> {
|
||||||
|
unsafe {
|
||||||
|
services::GLOBAL_MEMMAP_HANDLE = Some(services::file::mmap_mut_file(file_path)?);
|
||||||
|
services::GLOBAL_FILE_PATH = Some(file_path.to_string());
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
#[tauri::command]
|
pub mod app_error;
|
||||||
fn greet(name: &str) -> String {
|
pub mod commands;
|
||||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
pub mod pe_parse;
|
||||||
}
|
pub mod services;
|
||||||
|
|
||||||
#[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()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.invoke_handler(tauri::generate_handler![greet])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::command_open_file,
|
||||||
|
commands::command_get_file_pe_node_tree_data
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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")]
|
||||||
|
|
||||||
mod commands;
|
|
||||||
mod app_error;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
test_tauri_lib::run()
|
test_tauri_lib::run()
|
||||||
|
15
src-tauri/src/pe_parse/error.rs
Normal file
15
src-tauri/src/pe_parse/error.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PEParseError {
|
||||||
|
#[error("无效的DOSMagic值")]
|
||||||
|
InvalidDOSMagic,
|
||||||
|
#[error("Invalid NT header signature")]
|
||||||
|
InvalidNTSignature,
|
||||||
|
#[error("Invalid optional header magic number")]
|
||||||
|
InvalidOptionalMagic,
|
||||||
|
#[error("Invalid optional header size")]
|
||||||
|
InvalidOptionalSize,
|
||||||
|
#[error("解析超出了文件范围")]
|
||||||
|
OutOfBounds
|
||||||
|
}
|
113
src-tauri/src/pe_parse/header.rs
Normal file
113
src-tauri/src/pe_parse/header.rs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
use super::types::*;
|
||||||
|
use bitflags::bitflags;
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ImageDosHeader {
|
||||||
|
pub e_magic: u16, // Magic number 固定值 0x5A4D
|
||||||
|
pub e_cblp: u16,
|
||||||
|
pub e_cp: u16,
|
||||||
|
pub e_crlc: u16,
|
||||||
|
pub e_cparhdr: u16,
|
||||||
|
pub e_minalloc: u16,
|
||||||
|
pub e_maxalloc: u16,
|
||||||
|
pub e_ss: u16,
|
||||||
|
pub e_sp: u16,
|
||||||
|
pub e_csum: u16,
|
||||||
|
pub e_ip: u16,
|
||||||
|
pub e_cs: u16,
|
||||||
|
pub e_lfarlc: u16,
|
||||||
|
pub e_ovno: u16,
|
||||||
|
pub e_res: [u16; 4],
|
||||||
|
pub e_oemid: u16,
|
||||||
|
pub e_oeminfo: u16,
|
||||||
|
pub e_res2: [u16; 10],
|
||||||
|
pub e_lfanew: Offset, // File address of new exe header nt头的偏移
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ImageNTHeaders32 {
|
||||||
|
pub signature: u32,
|
||||||
|
pub file_header: ImageFileHeader,
|
||||||
|
pub optional_header: ImageOptionalHeader32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ImageFileHeader {
|
||||||
|
pub machine: u16,
|
||||||
|
pub number_of_sections: u16,
|
||||||
|
pub time_date_stamp: u32,
|
||||||
|
pub pointer_to_symbol_table: Offset,
|
||||||
|
pub number_of_symbols: u32,
|
||||||
|
pub size_of_optional_header: u16,
|
||||||
|
pub characteristics: FileCharacteristics,
|
||||||
|
}
|
||||||
|
bitflags! {
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct FileCharacteristics: u16 {
|
||||||
|
const RELOCS_STRIPPED = 0x0001;
|
||||||
|
const EXECUTABLE_IMAGE = 0x0002;
|
||||||
|
const LINE_NUMS_STRIPPED = 0x0004;
|
||||||
|
const LOCAL_SYMS_STRIPPED = 0x0008;
|
||||||
|
const AGGRESSIVE_WS_TRIM = 0x0010;
|
||||||
|
const LARGE_ADDRESS_AWARE = 0x0020;
|
||||||
|
const BYTES_REVERSED_LO = 0x0080;
|
||||||
|
const MACHINE_32BIT = 0x0100;
|
||||||
|
const DEBUG_STRIPPED = 0x0200;
|
||||||
|
const REMOVABLE_RUN_FROM_SWAP = 0x0400;
|
||||||
|
const NET_RUN_FROM_SWAP = 0x0800;
|
||||||
|
const SYSTEM = 0x1000;
|
||||||
|
const DLL = 0x2000;
|
||||||
|
const UP_SYSTEM_ONLY = 0x4000;
|
||||||
|
const BYTES_REVERSED_HI = 0x8000;
|
||||||
|
}
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct DLLCharacteristics: u16 {
|
||||||
|
const RESERVED1 = 0x0001;
|
||||||
|
const RESERVED2 = 0x0002;
|
||||||
|
const RESERVED4 = 0x0004;
|
||||||
|
const RESERVED8 = 0x0008;
|
||||||
|
const HIGH_ENTROPY_VA = 0x0020;
|
||||||
|
const DYNAMIC_BASE = 0x0040;
|
||||||
|
const FORCE_INTEGRITY = 0x0080;
|
||||||
|
const NX_COMPAT = 0x0100;
|
||||||
|
const NO_ISOLATION = 0x0200;
|
||||||
|
const NO_SEH = 0x0400;
|
||||||
|
const NO_BIND = 0x0800;
|
||||||
|
const APPCONTAINER = 0x1000;
|
||||||
|
const WDM_DRIVER = 0x2000;
|
||||||
|
const GUARD_CF = 0x4000;
|
||||||
|
const TERMINAL_SERVER_AWARE = 0x8000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ImageOptionalHeader32 {
|
||||||
|
pub magic: u16,
|
||||||
|
pub major_linker_version: u8,
|
||||||
|
pub minor_linker_version: u8,
|
||||||
|
pub size_of_code: u32,
|
||||||
|
pub size_of_initialized_data: u32,
|
||||||
|
pub size_of_uninitialized_data: u32,
|
||||||
|
pub address_of_entry_point: RVA,
|
||||||
|
pub base_of_code: RVA,
|
||||||
|
pub base_of_data: RVA,
|
||||||
|
pub image_base: u32,
|
||||||
|
pub section_alignment: u32,
|
||||||
|
pub file_alignment: u32,
|
||||||
|
pub major_operating_system_version: u16,
|
||||||
|
pub minor_operating_system_version: u16,
|
||||||
|
pub major_image_version: u16,
|
||||||
|
pub minor_image_version: u16,
|
||||||
|
pub major_subsystem_version: u16,
|
||||||
|
pub minor_subsystem_version: u16,
|
||||||
|
pub win32_version_value: u32,
|
||||||
|
pub size_of_image: u32,
|
||||||
|
pub size_of_headers: u32,
|
||||||
|
pub checksum: u32,
|
||||||
|
pub subsystem: u16,
|
||||||
|
pub dll_characteristics: DLLCharacteristics,
|
||||||
|
pub size_of_stack_reserve: u32,
|
||||||
|
pub size_of_stack_commit: u32,
|
||||||
|
pub size_of_heap_reserve: u32,
|
||||||
|
pub size_of_heap_commit: u32,
|
||||||
|
pub loader_flags: u32,
|
||||||
|
pub number_of_rva_and_sizes: u32,
|
||||||
|
}
|
4
src-tauri/src/pe_parse/mod.rs
Normal file
4
src-tauri/src/pe_parse/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod header;
|
||||||
|
pub mod types;
|
||||||
|
pub mod pe;
|
||||||
|
pub mod error;
|
35
src-tauri/src/pe_parse/pe.rs
Normal file
35
src-tauri/src/pe_parse/pe.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use super::{error::PEParseError, header::*};
|
||||||
|
|
||||||
|
pub trait PE: Deref<Target = [u8]> + DerefMut<Target = [u8]> + Sized {
|
||||||
|
/// Get a reference to a type at a given offset.
|
||||||
|
fn get_ref<T>(&self, offset: usize) -> Result<&T, PEParseError> {
|
||||||
|
let len = std::mem::size_of::<T>();
|
||||||
|
if offset + len > self.len() {
|
||||||
|
return Err(PEParseError::OutOfBounds);
|
||||||
|
}
|
||||||
|
let ptr = self.as_ptr().wrapping_offset(offset as isize) as *const T;
|
||||||
|
Ok(unsafe { &*ptr })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to a type at a given offset.
|
||||||
|
fn get_mut<T>(&mut self, offset: usize) -> Result<&mut T, PEParseError> {
|
||||||
|
let len = std::mem::size_of::<T>();
|
||||||
|
if offset + len > self.len() {
|
||||||
|
return Err(PEParseError::OutOfBounds);
|
||||||
|
}
|
||||||
|
let ptr = self.as_mut_ptr().wrapping_offset(offset as isize) as *mut T;
|
||||||
|
Ok(unsafe { &mut *ptr })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the DOS header without verifying its contents.
|
||||||
|
fn get_dos_header(&self) -> Result<&ImageDosHeader, PEParseError> {
|
||||||
|
let result = self.get_ref::<ImageDosHeader>(0)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
/// Get the DOS header without mutating its contents.
|
||||||
|
fn get_dos_header_mut(&mut self) -> Result<&mut ImageDosHeader, PEParseError> {
|
||||||
|
let result = self.get_mut::<ImageDosHeader>(0)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
4
src-tauri/src/pe_parse/types.rs
Normal file
4
src-tauri/src/pe_parse/types.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#[repr(C)]
|
||||||
|
pub struct Offset(pub u32);
|
||||||
|
|
||||||
|
pub struct RVA(pub u32);
|
15
src-tauri/src/services/file.rs
Normal file
15
src-tauri/src/services/file.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use memmap::*;
|
||||||
|
|
||||||
|
// TODO: 打开文件,并将文件映射到内存
|
||||||
|
pub fn mmap_mut_file(file_path: &str) -> Result<MmapMut, std::io::Error> {
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(file_path)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
MmapOptions::new()
|
||||||
|
.map_mut(&file)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
9
src-tauri/src/services/mod.rs
Normal file
9
src-tauri/src/services/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use memmap::MmapMut;
|
||||||
|
|
||||||
|
pub mod file;
|
||||||
|
|
||||||
|
// 映射到内存的文件句柄
|
||||||
|
pub static mut GLOBAL_MEMMAP_HANDLE: Option<MmapMut> = None;
|
||||||
|
|
||||||
|
// 全局文件路径
|
||||||
|
pub static mut GLOBAL_FILE_PATH: Option<String> = None;
|
115
src/App.css
115
src/App.css
@ -1,116 +1,7 @@
|
|||||||
.logo.vite:hover {
|
body {
|
||||||
filter: drop-shadow(0 0 2em #747bff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafb);
|
|
||||||
}
|
|
||||||
:root {
|
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 10vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: 0.75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.tauri:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #24c8db);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #ffffff;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border-color: #396cd8;
|
|
||||||
}
|
|
||||||
button:active {
|
|
||||||
border-color: #396cd8;
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#greet-input {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
color: #f6f6f6;
|
|
||||||
background-color: #2f2f2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #24c8db;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #0f0f0f98;
|
|
||||||
}
|
|
||||||
button:active {
|
|
||||||
background-color: #0f0f0f69;
|
|
||||||
}
|
}
|
||||||
|
.ant-layout{
|
||||||
|
background-color: #e5e5e5;
|
||||||
}
|
}
|
47
src/App.tsx
47
src/App.tsx
@ -1,50 +1,11 @@
|
|||||||
import { useState } from "react";
|
// import MainLayout from "./layouts/MainLayout";
|
||||||
import reactLogo from "./assets/react.svg";
|
import MainPage from "./pages/MainPage";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [greetMsg, setGreetMsg] = useState("");
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
|
||||||
async function greet() {
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
|
||||||
setGreetMsg(await invoke("greet", { name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container">
|
<MainPage></MainPage>
|
||||||
<h1>Welcome to Tauri + React</h1>
|
|
||||||
|
|
||||||
<div className="row">
|
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
|
||||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://tauri.app" target="_blank">
|
|
||||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://reactjs.org" target="_blank">
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className="row"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
greet();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="greet-input"
|
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
|
||||||
placeholder="Enter a name..."
|
|
||||||
/>
|
|
||||||
<button type="submit">Greet</button>
|
|
||||||
</form>
|
|
||||||
<p>{greetMsg}</p>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
src/components/DosHeader/DosHeader.module.scss
Normal file
8
src/components/DosHeader/DosHeader.module.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.root {
|
||||||
|
margin: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
230
src/components/DosHeader/DosHeader.tsx
Normal file
230
src/components/DosHeader/DosHeader.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import styles from "./DosHeader.module.scss";
|
||||||
|
import { Flex } from "antd";
|
||||||
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import type { GetRef, InputRef, TableProps } from "antd";
|
||||||
|
import { Button, Form, Input, Popconfirm, Table } from "antd";
|
||||||
|
|
||||||
|
type FormInstance<T> = GetRef<typeof Form<T>>;
|
||||||
|
|
||||||
|
const EditableContext = React.createContext<FormInstance<any> | null>(null);
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
age: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditableRowProps {
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
return (
|
||||||
|
<Form form={form} component={false}>
|
||||||
|
<EditableContext.Provider value={form}>
|
||||||
|
<tr {...props} />
|
||||||
|
</EditableContext.Provider>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EditableCellProps {
|
||||||
|
title: React.ReactNode;
|
||||||
|
editable: boolean;
|
||||||
|
dataIndex: keyof Item;
|
||||||
|
record: Item;
|
||||||
|
handleSave: (record: Item) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableCell: React.FC<React.PropsWithChildren<EditableCellProps>> = ({
|
||||||
|
title,
|
||||||
|
editable,
|
||||||
|
children,
|
||||||
|
dataIndex,
|
||||||
|
record,
|
||||||
|
handleSave,
|
||||||
|
...restProps
|
||||||
|
}) => {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
const form = useContext(EditableContext)!;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [editing]);
|
||||||
|
|
||||||
|
const toggleEdit = () => {
|
||||||
|
setEditing(!editing);
|
||||||
|
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
toggleEdit();
|
||||||
|
handleSave({ ...record, ...values });
|
||||||
|
} catch (errInfo) {
|
||||||
|
console.log("Save failed:", errInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let childNode = children;
|
||||||
|
|
||||||
|
if (editable) {
|
||||||
|
childNode = editing ? (
|
||||||
|
<Form.Item
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
name={dataIndex}
|
||||||
|
rules={[{ required: true, message: `${title} is required.` }]}
|
||||||
|
>
|
||||||
|
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="editable-cell-value-wrap"
|
||||||
|
style={{ paddingInlineEnd: 24 }}
|
||||||
|
onClick={toggleEdit}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <td {...restProps}>{childNode}</td>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DataType {
|
||||||
|
key: React.Key;
|
||||||
|
name: string;
|
||||||
|
age: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnTypes = Exclude<TableProps<DataType>["columns"], undefined>;
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const [dataSource, setDataSource] = useState<DataType[]>([
|
||||||
|
{
|
||||||
|
key: "0",
|
||||||
|
name: "Edward King 0",
|
||||||
|
age: "32",
|
||||||
|
address: "London, Park Lane no. 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
name: "Edward King 1",
|
||||||
|
age: "32",
|
||||||
|
address: "London, Park Lane no. 1",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [count, setCount] = useState(2);
|
||||||
|
|
||||||
|
const handleDelete = (key: React.Key) => {
|
||||||
|
const newData = dataSource.filter((item) => item.key !== key);
|
||||||
|
setDataSource(newData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultColumns: (ColumnTypes[number] & {
|
||||||
|
editable?: boolean;
|
||||||
|
dataIndex: string;
|
||||||
|
})[] = [
|
||||||
|
{
|
||||||
|
title: "name",
|
||||||
|
dataIndex: "name",
|
||||||
|
width: "30%",
|
||||||
|
editable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "age",
|
||||||
|
dataIndex: "age",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "address",
|
||||||
|
dataIndex: "address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "operation",
|
||||||
|
dataIndex: "operation",
|
||||||
|
render: (_, record) =>
|
||||||
|
dataSource.length >= 1 ? (
|
||||||
|
<Popconfirm
|
||||||
|
title="Sure to delete?"
|
||||||
|
onConfirm={() => handleDelete(record.key)}
|
||||||
|
>
|
||||||
|
<a>Delete</a>
|
||||||
|
</Popconfirm>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
const newData: DataType = {
|
||||||
|
key: count,
|
||||||
|
name: `Edward King ${count}`,
|
||||||
|
age: "32",
|
||||||
|
address: `London, Park Lane no. ${count}`,
|
||||||
|
};
|
||||||
|
setDataSource([...dataSource, newData]);
|
||||||
|
setCount(count + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (row: DataType) => {
|
||||||
|
const newData = [...dataSource];
|
||||||
|
const index = newData.findIndex((item) => row.key === item.key);
|
||||||
|
const item = newData[index];
|
||||||
|
newData.splice(index, 1, {
|
||||||
|
...item,
|
||||||
|
...row,
|
||||||
|
});
|
||||||
|
setDataSource(newData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
body: {
|
||||||
|
row: EditableRow,
|
||||||
|
cell: EditableCell,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = defaultColumns.map((col) => {
|
||||||
|
if (!col.editable) {
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
onCell: (record: DataType) => ({
|
||||||
|
record,
|
||||||
|
editable: col.editable,
|
||||||
|
dataIndex: col.dataIndex,
|
||||||
|
title: col.title,
|
||||||
|
handleSave,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table<DataType>
|
||||||
|
components={components}
|
||||||
|
rowClassName={() => "editable-row"}
|
||||||
|
bordered
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
dataSource={dataSource}
|
||||||
|
columns={columns as ColumnTypes}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DosHeader() {
|
||||||
|
return (
|
||||||
|
<Flex className={styles.root}>
|
||||||
|
<App />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
8
src/components/side_tree/SideTree.module.scss
Normal file
8
src/components/side_tree/SideTree.module.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.root {
|
||||||
|
background-color: white;
|
||||||
|
min-height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
112
src/components/side_tree/SideTree.tsx
Normal file
112
src/components/side_tree/SideTree.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import styles from "./SideTree.module.scss";
|
||||||
|
import { Flex } from "antd";
|
||||||
|
import { Tree } from "antd";
|
||||||
|
import { DownOutlined } from "@ant-design/icons";
|
||||||
|
import type { TreeProps } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// const treeData: TreeDataNode[] = [
|
||||||
|
// {
|
||||||
|
// title: "文件: Test.exe",
|
||||||
|
// key: "0-0",
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// title: "Dos 头部",
|
||||||
|
// key: "0-0-0",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Nt 头部",
|
||||||
|
// key: "0-0-1",
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// title: "文件头部",
|
||||||
|
// key: "0-0-1-0",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "可选头部",
|
||||||
|
// key: "0-0-2-0",
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// title: "数据目录",
|
||||||
|
// key: "0-0-1-1",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "节 头部",
|
||||||
|
// key: "0-0-2",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "导入目录",
|
||||||
|
// key: "0-0-3",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "资源目录",
|
||||||
|
// key: "0-0-4",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "地址转换",
|
||||||
|
// key: "0-0-5",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "依赖性分析",
|
||||||
|
// key: "0-0-6",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
export default function SiderTree({ treeData, defaultSelectedKey }) {
|
||||||
|
// 展开的节点
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 受控 选择的节点
|
||||||
|
const [selectedKey, setSelectedKey] = useState<string>(defaultSelectedKey);
|
||||||
|
|
||||||
|
const onSelect: TreeProps["onSelect"] = (selectedKeys, info) => {
|
||||||
|
console.log("selected", selectedKeys, info);
|
||||||
|
setSelectedKey(selectedKeys[0] as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExpand: TreeProps["onExpand"] = (_expandedKeys) => {
|
||||||
|
setExpandedKeys(_expandedKeys as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 每次 treeData 变化时,更新 expandedKeys
|
||||||
|
useEffect(() => {
|
||||||
|
// 递归获取所有的 key
|
||||||
|
const getKeys = (nodes) => {
|
||||||
|
const keys = nodes.map((node) => node.key);
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.children) {
|
||||||
|
keys.push(...getKeys(node.children));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
setExpandedKeys(getKeys(treeData));
|
||||||
|
// 设置默认选中的节点
|
||||||
|
setSelectedKey(defaultSelectedKey);
|
||||||
|
}, [treeData]);
|
||||||
|
console.log("selectedKey", selectedKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex className={styles.root}>
|
||||||
|
<Tree
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
showLine
|
||||||
|
treeData={treeData}
|
||||||
|
selectedKeys={[selectedKey]}
|
||||||
|
switcherIcon={<DownOutlined />}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onExpand={onExpand}
|
||||||
|
expandedKeys={expandedKeys}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
25
src/pages/MainPage.module.scss
Normal file
25
src/pages/MainPage.module.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.mainLayout {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sider {
|
||||||
|
width: 250px;
|
||||||
|
height: 100%;
|
||||||
|
margin: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: white;
|
||||||
|
// 圆角
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background-color: white;
|
||||||
|
height: 55px;
|
||||||
|
// 圆角
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
55
src/pages/MainPage.tsx
Normal file
55
src/pages/MainPage.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import styles from "./MainPage.module.scss";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Layout, Flex, Button } from "antd";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
const { Content } = Layout;
|
||||||
|
import { SaveOutlined } from "@ant-design/icons";
|
||||||
|
import SiderTree from "../components/side_tree/SideTree";
|
||||||
|
import DosHeader from "../components/DosHeader/DosHeader";
|
||||||
|
|
||||||
|
export default function MainPage() {
|
||||||
|
const [treeData, setTreeData] = useState([]);
|
||||||
|
const [filePath, setFilePath] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen<{ paths: Array<string> }>(
|
||||||
|
"tauri://drag-drop",
|
||||||
|
(event) => {
|
||||||
|
const filePath = event.payload.paths[0];
|
||||||
|
console.log("file path:", filePath);
|
||||||
|
setFilePath(filePath);
|
||||||
|
invoke("command_open_file", { filePath }).then(() => {
|
||||||
|
invoke("command_get_file_pe_node_tree_data").then((res) => {
|
||||||
|
console.log("tree nodes:", res);
|
||||||
|
setTreeData(res as any);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
unlisten.then((f) => f());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout className={styles.mainLayout}>
|
||||||
|
<Flex className={styles.header}>
|
||||||
|
<Button type="primary" icon={<SaveOutlined></SaveOutlined>}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex className={styles.sider}>
|
||||||
|
<SiderTree
|
||||||
|
treeData={treeData}
|
||||||
|
defaultSelectedKey={treeData[0]?.key || ""}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Content>
|
||||||
|
<DosHeader></DosHeader>
|
||||||
|
</Content>
|
||||||
|
</Flex>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
@ -13,9 +13,9 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"noImplicitAny": false,
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": false,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
|
Loading…
Reference in New Issue
Block a user