feat: 添加前后端交互的示例代码
This commit is contained in:
119
src/App.css
119
src/App.css
@@ -1,116 +1,7 @@
|
||||
.logo.vite:hover {
|
||||
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 {
|
||||
body {
|
||||
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 reactLogo from "./assets/react.svg";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
// import MainLayout from "./layouts/MainLayout";
|
||||
import MainPage from "./pages/MainPage";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
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 (
|
||||
<main className="container">
|
||||
<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>
|
||||
<MainPage></MainPage>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user