环境#
System:Windows 11
IDE:VSCode + Github Copilot Chat + Rust + Tauri 插件
Env:npm、Rust
已经通过npm create tauri-app@latest
建立项目框架
以下标题均为在 VSCode 的 copilot chat 中输入的文本,然后对 Copilot 输出的代码进行更新替换。
将首页修改为:左侧为导航区域,以树状视图,显示不同功能的页面链接;点击左侧导航区域的功能菜单项后,右侧加载对应的功能页面#
Note
这里引用了 'App.tsx'
GitHub Copilot: 要实现这个功能,我们需要在App.tsx
中进行以下步骤:
- 创建一个导航栏组件,使用树状视图显示不同功能的页面链接。
- 使用 React Router 来处理页面导航。
- 在右侧区域加载对应的功能页面。
首先,安装 React Router:
npm install react-router-dom
然后,修改App.tsx
文件如下:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
function Home() {
return <h2>Home Page</h2>;
}
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
同时,更新 App.css
文件以适应新的布局:
.container {
display: flex;
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
}
.content {
flex-grow: 1;
padding: 10px;
}
这样,左侧将显示导航栏,右侧将根据导航栏的选择加载相应的功能页面。
Similar code found with 1 license type
创建独立的 Home 组件#
GitHub Copilot: 好的,我们可以将 Home
组件提取到一个独立的文件中。以下是步骤:
- 创建一个新的文件
Home.tsx
。 - 将
Home
组件的代码移到Home.tsx
文件中。 - 在
App.tsx
中导入Home
组件。
首先,创建 Home.tsx
文件,并添加以下代码:
// Home.tsx
import React from "react";
function Home() {
return <h2>Home Page</h2>;
}
export default Home;
然后,修改 App.tsx
文件,导入 Home
组件:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,Home
组件就被提取到一个独立的文件中了。
Similar code found with 1 license type
左侧导航栏垂直拉伸到占满页面#
Note
左侧导航栏只占据了包含文本的区域,因此希望能全部占满。
GitHub Copilot: 为了使左侧导航栏垂直拉伸到占满页面,我们需要调整 CSS 样式。具体步骤如下:
- 修改
App.css
文件,确保导航栏占满页面高度。 - 确保容器的高度也占满页面。
以下是更新后的 App.css
文件:
/* App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
}
确保 App.tsx
文件中包含以下代码:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,左侧导航栏将垂直拉伸占满页面高度。
Similar code found with 1 license type
右侧页面和左侧导航栏应该顶部对齐#
Note
出现右侧内容区显示在导航区的下方的情形,4,5,6 均为尝试修正的 Promt,后来发现是 css 中.container 的样式没有完全替换,重新替换后正常显示
GitHub Copilot: 为了确保右侧页面和左侧导航栏顶部对齐,我们需要调整 CSS 样式。具体步骤如下:
- 确保容器的高度占满页面,并且导航栏和内容区域都从顶部开始对齐。
- 更新
App.css
文件以确保布局正确。
以下是更新后的 App.css
文件:
/* App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* 确保子元素顶部对齐 */
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
}
确保 App.tsx
文件中包含以下代码:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,右侧页面和左侧导航栏将会顶部对齐。
Similar code found with 1 license type
.container 内的子元素按照左右布局排列#
GitHub Copilot: 为了确保 .container
内的子元素按照左右布局排列,我们需要调整 CSS 样式。具体步骤如下:
- 确保
.container
使用flex
布局,并且子元素按照左右排列。 - 移除重复的
.container
样式定义。
以下是更新后的 App.css
文件:
/* App.css */
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* 确保子元素顶部对齐 */
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
}
确保 App.tsx
文件中包含以下代码:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,.container
内的子元素将按照左右布局排列,并且顶部对齐。
Similar code found with 1 license type
导航栏的背景颜色设置为系统背景色#
Note
系统主题是暗色,但默认背景色为白色,且这个代码未实际生效,最后手动删除了.sidebar
的背景色配置
GitHub Copilot: 为了将导航栏的背景颜色设置为系统背景色,可以使用 CSS 中的 background-color
属性,并将其值设置为 background
系统颜色。以下是更新后的 App.css
文件:
/* App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* 确保子元素顶部对齐 */
}
.sidebar {
width: 200px;
background-color: var(--system-background-color, #f0f0f0); /* 使用系统背景色 */
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
}
.logo {
height: 6em;
}
在上面的代码中,var(--system-background-color, #f0f0f0)
使用了 CSS 变量来设置背景颜色。如果系统背景色未定义,则使用默认颜色 #f0f0f0
。你可以根据需要调整默认颜色。
将 Feature1 单独放入 Features 子目录下的独立模块中#
GitHub Copilot: 好的,我们可以将 Feature1
组件提取到一个独立的文件中,并将其放入 Features
子目录中。以下是步骤:
- 创建一个新的目录
Features
。 - 在
Features
目录中创建一个新的文件Feature1.tsx
。 - 将
Feature1
组件的代码移到Feature1.tsx
文件中。 - 在
App.tsx
中导入Feature1
组件。
首先,创建 Features/Feature1.tsx
文件,并添加以下代码:
// Features/Feature1.tsx
import React from "react";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
export default Feature1;
然后,修改
App.tsx
文件,导入
Feature1
组件:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,
Feature1
组件就被提取到 Features
子目录中的独立模块中了。
Similar code found with 1 license type
同样将 Feature2 移入独立文件#
GitHub Copilot: 好的,我们可以将
Feature2
组件提取到一个独立的文件中,并将其放入 Features
子目录中。以下是步骤:
- 在
Features
目录中创建一个新的文件
Feature2.tsx
。
2. 将
Feature2
组件的代码移到
Feature2.tsx
文件中。
3. 在
App.tsx
中导入
Feature2
组件。
首先,创建 Features/Feature2.tsx
文件,并添加以下代码:
// Features/Feature2.tsx
import React from "react";
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
export default Feature2;
然后,修改
App.tsx
文件,导入
Feature2
组件:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,
Feature2
组件就被提取到 Features
子目录中的独立模块中了。
Similar code found with 1 license type
页面默认包含滚动条,将页面高度调整到不出现滚动条#
Note
默认内容区出现了滚动条
GitHub Copilot: 为了确保页面高度调整到不出现滚动条,我们需要确保容器的高度和内容的高度都正确设置。我们可以使用 overflow: hidden
来防止滚动条的出现,并确保内容区域的高度不会超过视口高度。
以下是更新后的
App.css
文件:
/* App.css */
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* 防止滚动条出现 */
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* 确保子元素顶部对齐 */
}
.sidebar {
width: 200px;
padding: 10px;
height: 100%;
background-color: var(--system-background-color, #f0f0f0); /* 使用系统背景色 */
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
overflow: auto; /* 允许内容区域滚动 */
}
确保 App.tsx
文件中包含以下代码:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,页面的高度将调整到不出现滚动条,并且内容区域允许滚动。
Similar code found with 1 license type
greetMsg 的段落显示在新的一行#
Note
这里原因是为了保留示例中的 greet 交互,手动将代码从 App.tsx 移到了 Home.tsc 中,但是<p>
标签不生效,和文本输出栏显示在同一行。通过下一 Promt,以<div>
标签来解决了问题。
GitHub Copilot: 为了确保
greetMsg
显示在新的一行,我们可以将其放在一个段落 (<p>
) 标签中。以下是更新后的
Home.tsx
文件:
// Home.tsx
import React from "react";
function Home({ greetMsg }: { greetMsg: string }) {
return (
<div>
<h2>Home Page</h2>
<p>{greetMsg}</p>
</div>
);
}
export default Home;
同时,确保在 App.tsx
中传递
greetMsg
作为 Home
组件的属性:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home greetMsg={greetMsg} />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,
greetMsg
将显示在 Home
组件中的新的一行。
Similar code found with 1 license type
greetMsg 无须从 App 中传入#
GitHub Copilot: 好的,我们可以在
Home
组件中直接定义 greetMsg
,而不需要从
App
中传入。以下是更新后的
Home.tsx
文件:
// Home.tsx
import React, { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
function Home() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<div>
<h2>Home Page</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<button onClick={greet}>Greet</button>
<p>{greetMsg}</p>
</div>
);
}
export default Home;
同时,
App.tsx
文件不需要任何修改,因为 greetMsg
不再需要从
App
中传入:
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,greetMsg
将在
Home
组件中定义和使用。
Similar code found with 2 license types
将 Feature1 重命名为 ClipBoard#
GitHub Copilot: 好的,我们可以将
Feature1
组件重命名为 ClipBoard
。以下是步骤:
- 重命名文件
Feature1.tsx
为 Features/ClipBoard.tsx
。
2. 更新组件名称为 ClipBoard
。
3. 在 App.tsx
中导入 ClipBoard
组件。
首先,重命名文件
Feature1.tsx
为 Features/ClipBoard.tsx
,并更新文件内容如下:
// Features/ClipBoard.tsx
import React from "react";
function ClipBoard() {
return <h2>ClipBoard Page</h2>;
}
export default ClipBoard;
然后,修改 App.tsx
文件,导入 ClipBoard
组件:
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import "./App.css";
import Home from "./Home";
import ClipBoard from "./Features/ClipBoard";
import Feature2 from "./Features/Feature2";
function App() {
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/clipboard">ClipBoard</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/clipboard" element={<ClipBoard />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
这样,
Feature1
组件就被重命名为 ClipBoard
并且在 App.tsx
中正确导入和使用了。
剪贴板页面,显示最近的剪贴板内容,按照时间倒序排列,最多显示 100 条#
GitHub Copilot: 为了实现剪贴板页面显示最近的剪贴板内容,并按照时间倒序排列,最多显示 100 条,我们需要以下步骤:
- 创建一个状态来存储剪贴板内容。
- 使用
useEffect
钩子来模拟获取剪贴板内容。 - 按时间倒序排列剪贴板内容,并限制显示 100 条。
以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
useEffect(() => {
// 模拟获取剪贴板内容
const fetchClipboardItems = () => {
const items = [
{ content: "Item 1", timestamp: Date.now() - 1000 },
{ content: "Item 2", timestamp: Date.now() - 2000 },
// 添加更多模拟数据
];
setClipboardItems(items);
};
fetchClipboardItems();
}, []);
// 按时间倒序排列,并限制显示100条
const sortedItems = clipboardItems
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<p>{item.content}</p>
<small>{new Date(item.timestamp).toLocaleString()}</small>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 定义了一个
ClipboardItem
接口来表示剪贴板内容项。 - 使用
useState
钩子来存储剪贴板内容。 - 使用
useEffect
钩子来模拟获取剪贴板内容,并将其存储在状态中。 - 按时间倒序排列剪贴板内容,并限制显示 100 条。
- 在组件中渲染剪贴板内容列表。
你可以根据实际需求替换模拟数据部分,获取真实的剪贴板内容。
定时每隔 1s 读取一次剪贴板,如果存在新内容,则添加到剪贴板的内容列表中#
Note
这里需要手动在src-tauri/capabilities/default.json
中添加剪贴板的读写权限,后续文件操作相同。同时 copilot 代码中的import
还是 Tauri v1 的格式,需要手动调整为 v2 的格式。
GitHub Copilot: 为了实现定时每隔 1 秒读取一次剪贴板,并在存在新内容时将其添加到剪贴板的内容列表中,我们可以使用 setInterval
来定时读取剪贴板内容,并在
useEffect
中进行处理。
以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText } from "@tauri-apps/plugin-clipboard";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
setClipboardItems((prevItems) => [
{ content, timestamp: Date.now() },
...prevItems,
]);
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
// 按时间倒序排列,并限制显示100条
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<p>{item.content}</p>
<small>{new Date(item.timestamp).toLocaleString()}</small>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 使用
useState
钩子来存储剪贴板内容和最后一次读取的内容。
2. 使用
useEffect
钩子来设置一个定时器,每隔 1 秒读取一次剪贴板内容。
3. 如果读取到的新内容与最后一次读取的内容不同,则将其添加到剪贴板内容列表中,并更新最后一次读取的内容。
4. 按时间倒序排列剪贴板内容,并限制显示 100 条。
5. 在组件中渲染剪贴板内容列表。
这样,每隔 1 秒读取一次剪贴板内容,并在存在新内容时将其添加到剪贴板的内容列表中。
将时间显示在内容的上方#
GitHub Copilot: 为了将时间显示在内容的上方,我们需要调整 ClipBoard.tsx
文件中的渲染顺序。以下是更新后的代码:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText } from "@tauri-apps/plugin-clipboard";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
setClipboardItems((prevItems) => [
{ content, timestamp: Date.now() },
...prevItems,
]);
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
// 按时间倒序排列,并限制显示100条
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们将时间 (<small>{new Date(item.timestamp).toLocaleString()}</small>
) 放在内容 (<p>{item.content}</p>
) 的上方。这样,时间将显示在内容的上方。
每项内容,提供 “复制” 和 "删除" 的文本链接,点击复制时,重新复制到剪贴板,点击删除时,从列表中移除该项#
GitHub Copilot: 为了实现每项内容提供 “复制” 和 “删除” 的文本链接,我们需要以下步骤:
- 添加 “复制” 和 “删除” 按钮。
- 实现复制功能,将内容重新复制到剪贴板。
- 实现删除功能,从列表中移除该项。
以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
setClipboardItems((prevItems) => [
{ content, timestamp: Date.now() },
...prevItems,
]);
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
// 按时间倒序排列,并限制显示100条
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 添加了 “复制” 和 “删除” 按钮。
- 实现了
handleCopy
函数,将内容重新复制到剪贴板。 - 实现了
handleDelete
函数,从列表中移除该项。 - 在每个列表项中添加了 “复制” 和 “删除” 链接,并绑定相应的事件处理函数。
这样,每项内容将提供 “复制” 和 “删除” 的文本链接,点击复制时,重新复制到剪贴板,点击删除时,从列表中移除该项。
在列表中同时保存内容文本和内容的 md5 值,当一个新的内容,md5 值和已有的内容文本一致时,仅仅更新时间,而不是新增一条记录#
Note
复制后会产生新的一条记录,因此通过 md5 来区分,确保唯一性显示。
GitHub Copilot: 为了在列表中同时保存内容文本和内容的 MD5 值,并在新内容的 MD5 值与已有内容文本一致时仅更新时间而不是新增一条记录,我们需要以下步骤:
- 安装
crypto-js
库来计算 MD5 值。 - 修改
ClipboardItem
接口以包含 MD5 值。 - 在读取剪贴板内容时计算 MD5 值,并检查是否已有相同的 MD5 值。
首先,安装 crypto-js
库:
npm install crypto-js
然后,更新 ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent
)
{
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
return prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
} else {
return [{ content, md5, timestamp: Date.now() }, ...prevItems];
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
// 按时间倒序排列,并限制显示100条
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 安装并导入了
crypto-js
库来计算 MD5 值。 - 更新了
ClipboardItem
接口以包含md5
属性。 - 在读取剪贴板内容时计算 MD5 值,并检查是否已有相同的 MD5 值。如果有相同的 MD5 值,则仅更新时间;否则,新增一条记录。
相同内容更新时间后,列表要重新按时间排序#
GitHub Copilot: 为了确保在相同内容更新时间后,列表重新按时间排序,我们需要在更新时间戳后对列表进行排序。以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md
5
= CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 在更新时间戳后,对列表进行排序。
- 确保在添加新内容时,列表也进行排序并限制显示 100 条。
这样,当相同内容更新时间后,列表将重新按时间排序。
重新排序后,页面也要按照新的顺序重新刷新显示#
Note
排序后,页面并未刷新,因此要求强制刷新。
GitHub Copilot: 为了确保在重新排序后页面按照新的顺序重新刷新显示,我们需要确保在 setClipboardItems
中对列表进行排序,并且在渲染时使用排序后的列表。
以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== last
Content
) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们确保在 setClipboardItems
中对列表进行排序,并且在渲染时使用排序后的 clipboardItems
列表。这样,当列表重新排序后,页面将按照新的顺序重新刷新显示。
相邻的两行以不同的背景颜色进行区分#
Note
UI 优化。
GitHub Copilot: 为了实现相邻的两行以不同的背景颜色进行区分,我们可以使用 CSS 样式来实现条纹效果。以下是更新后的
ClipBoard.tsx
文件和相应的 CSS 样式:
首先,更新
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // 导入 CSS 文件
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent
)
{
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
然后,创建
ClipBoard.css
文件,并添加以下样式:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #f9f9f9;
}
li.odd {
background-color: #e9e9e9;
}
li {
padding: 10px;
margin: 5px 0;
}
a {
margin-right: 10px;
}
在这个实现中,我们:
- 在
ClipBoard.tsx
文件中导入了
ClipBoard.css
文件。
2. 使用 className
属性为相邻的两行添加不同的 CSS 类 (even
和 odd
)。
3. 在
ClipBoard.css
文件中定义了 even
和 odd
类的背景颜色。
这样,相邻的两行将以不同的背景颜色进行区分。
不是相邻的两行,而是相邻的两个剪贴板内容以不同颜色区分,包括文本内容、时间和操作按钮#
GitHub Copilot: 为了确保相邻的两个剪贴板内容以不同颜色区分,包括文本内容、时间和操作按钮,我们可以使用 CSS 样式来实现条纹效果。以下是更新后的
ClipBoard.tsx
文件和相应的 CSS 样式:
首先,更新
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // 导入 CSS 文件
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5
=
CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</div>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
然后,创建或更新
ClipBoard.css
文件,并添加以下样式:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #f9f9f9;
}
li.odd {
background-color: #e9e9e9;
}
li {
padding: 10px;
margin: 5px 0;
}
a {
margin-right: 10px;
}
在这个实现中,我们:
- 在
ClipBoard.tsx
文件中导入了
ClipBoard.css
文件。
2. 使用
className
属性为相邻的两个剪贴板内容添加不同的 CSS 类 (even
和 odd
)。
3. 在
ClipBoard.css
文件中定义了 even
和 odd
类的背景颜色。
这样,相邻的两个剪贴板内容将以不同的背景颜色进行区分,包括文本内容、时间和操作按钮。
内容的背景色调调整为暗色调,文本调整为对比度高的颜色#
GitHub Copilot: 为了将内容的背景色调调整为暗色调,并将文本调整为对比度高的颜色,我们可以更新
ClipBoard.css
文件中的样式。以下是更新后的
ClipBoard.css
文件:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #333; /* 暗色调背景 */
color: #fff; /* 高对比度文本颜色 */
}
li.odd {
background-color: #444; /* 暗色调背景 */
color: #fff; /* 高对比度文本颜色 */
}
li {
padding: 10px;
margin: 5px 0;
}
a {
margin-right: 10px;
color: #1e90ff; /* 高对比度链接颜色 */
}
a:hover {
text-decoration: underline;
}
在这个实现中,我们:
- 将
li.even
和li.odd
的背景颜色分别设置为#333
和#444
,以实现暗色调背景。 - 将
li.even
和li.odd
的文本颜色设置为#fff
,以实现高对比度文本颜色。 - 将链接的颜色设置为
#1e90ff
,以实现高对比度链接颜色,并在悬停时添加下划线。
这样,内容的背景色调将调整为暗色调,文本将调整为对比度高的颜色。
将时间和操作按钮合并到一行#
GitHub Copilot: 为了将时间和操作按钮合并到一行,我们可以使用 CSS Flexbox 布局来实现。以下是更新后的 ClipBoard.tsx
文件和相应的 CSS 样式:
首先,更新 ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // 导入 CSS 文件
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent
)
{
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</div>
</div>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
然后,更新
ClipBoard.css
文件,并添加以下样式:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #333; /* 暗色调背景 */
color: #fff; /* 高对比度文本颜色 */
}
li.odd {
background-color: #444; /* 暗色调背景 */
color: #fff; /* 高对比度文本颜色 */
}
li {
padding: 10px;
margin: 5px 0;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
a {
margin-right: 10px;
color: #1e90ff; /* 高对比度链接颜色 */
}
a:hover {
text-decoration: underline;
}
在这个实现中,我们:
- 在
ClipBoard.tsx
文件中将时间和操作按钮放在一个div
容器中,并添加item-header
类。 - 在
ClipBoard.css
文件中定义了 item-header
类,使用 Flexbox 布局将时间和操作按钮合并到一行,并在两端对齐。
这样,时间和操作按钮将合并到一行,并且内容的背景色调调整为暗色调,文本调整为对比度高的颜色。
以 1 分钟的间隔,定时将当前内容列表以及时间信息保存到 appdata 下的 history.json 文件中#
Note
持久化保存历史记录。这里的读写 API 同样都是输出了 v1 的格式,因此需要手动调整为 v2 的 API 调用方式,通过 VSCode 的 tauri 插件,可以 Ctrl + 单击函数名称打开 API 的源码,其注释中给出了调用参考。
GitHub Copilot: 为了以 1 分钟的间隔定时将当前内容列表以及时间信息保存到 appdata
下的 history.json
文件中,我们需要以下步骤:
- 使用
fs
模块来写入文件。 - 使用
setInterval
来定时保存内容列表。 - 确保在组件卸载时清除定时器。
以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { writeTextFile, BaseDirectory } from "@tauri-apps/api/fs";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // 导入 CSS 文件
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md
5
= CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
useEffect(() => {
const saveInterval = setInterval(async () => {
const data = JSON.stringify(clipboardItems, null, 2);
await writeTextFile({ path: "history.json", contents: data }, { dir: BaseDirectory.AppData });
}, 60000); // 1分钟间隔
return () => clearInterval(saveInterval);
}, [clipboardItems]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</div>
</div>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 使用
@tauri-apps/api/fs
模块中的writeTextFile
函数来写入文件。 - 使用
useEffect
钩子设置一个定时器,每隔 1 分钟将当前内容列表保存到appdata
下的history.json
文件中。 - 确保在组件卸载时清除定时器。
这样,每隔 1 分钟,当前内容列表以及时间信息将被保存到 appdata
下的 history.json
文件中。
在首次进入剪贴板页面时,从 AppData 目录下读取 history.json 文件,并将内容解析为剪贴板的历史内容,显示在界面上#
GitHub Copilot: 为了在首次进入剪贴板页面时从 AppData
目录下读取 history.json
文件,并将内容解析为剪贴板的历史内容显示在界面上,我们需要以下步骤:
- 使用
fs
模块来读取文件。 - 在组件挂载时读取
history.json
文件并解析内容。 - 将解析后的内容设置为剪贴板的历史内容。
以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // 导入 CSS 文件
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const loadHistory = async () => {
try {
const data = await readTextFile("history.json", { dir: BaseDirectory.AppData });
const items: ClipboardItem[] = JSON.parse(data);
setClipboardItems(items);
} catch (error) {
console.error("Failed to load history:", error);
}
};
loadHistory();
}, []);
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
useEffect(() => {
const saveInterval = setInterval(async () => {
const data = JSON.stringify(clipboardItems, null, 2);
await writeTextFile({ path: "history.json", contents: data }, { dir: BaseDirectory.AppData });
}, 60000); // 1分钟间隔
return () => clearInterval(saveInterval);
}, [clipboardItems]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</div>
</div>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 使用
@tauri-apps/plugin-fs
模块中的readTextFile
函数来读取文件。 - 在组件挂载时(
useEffect
钩子)读取 history.json
文件并解析内容。
3. 将解析后的内容设置为剪贴板的历史内容,并显示在界面上。
这样,在首次进入剪贴板页面时,将从 AppData
目录下读取 history.json
文件,并将内容解析为剪贴板的历史内容显示在界面上。
增加 ClipboardItems map 到界面元素的异常处理,忽略无效的数据#
Note
首次打开时没有 history.json 文件,手动创建并构建一个空 json 后抛出异常,最后手动加入检查 items 为有效数组时才赋值的代码后通过。
GitHub Copilot: 为了增加 clipboardItems
映射到界面元素的异常处理,并忽略无效的数据,我们可以在渲染列表项时添加错误处理逻辑。以下是更新后的
ClipBoard.tsx
文件:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // 导入 CSS 文件
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const loadHistory = async () => {
try {
const data = await readTextFile("history.json", { dir: BaseDirectory.AppData });
const items: ClipboardItem[] = JSON.parse(data);
setClipboardItems(items);
} catch (error) {
console.error("Failed to load history:", error);
}
};
loadHistory();
}, []);
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
useEffect(() => {
const saveInterval = setInterval(async () => {
const data = JSON.stringify(clipboardItems, null, 2);
await writeTextFile({ path: "history.json", contents: data }, { dir: BaseDirectory.AppData });
}, 60000); // 1分钟间隔
return () => clearInterval(saveInterval);
}, [clipboardItems]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => {
try {
if (!item.content || !item.timestamp) {
throw new Error("Invalid item");
}
return (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>复制</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>删除</a>
</div>
</div>
<p>{item.content}</p>
</li>
);
} catch (error) {
console.error("Invalid clipboard item:", error);
return null;
}
})}
</ul>
</div>
);
}
export default ClipBoard;
在这个实现中,我们:
- 在
clipboardItems.map
中添加了错误处理逻辑。 - 检查每个
item
是否包含有效的content
和timestamp
属性。 - 如果
item
无效,则抛出错误并在控制台中记录错误信息,同时返回null
以忽略无效的数据。
这样,在映射 clipboardItems
到界面元素时,将会忽略无效的数据,并在控制台中记录错误信息。
只列出之前发出的所有指令,不显示代码更新内容#
Note
这里的总结只给出了功能调整的指令,项目结构和 UI 优化的指令都被忽略了。
GitHub Copilot:
- 创建独立的
Home
组件。 - 将
Feature1
移入Features
子目录中的独立模块。 - 将
Feature1
重命名为ClipBoard
。 - 显示最近的剪贴板内容,按照时间倒序排列,最多显示 100 条。
- 将
greetMsg
显示在新的一行。 - 定时每隔 1 秒读取一次剪贴板,如果存在新内容,则添加到剪贴板的内容列表中。
- 在列表中同时保存内容文本和内容的 MD5 值,当一个新的内容,MD5 值和已有的内容文本一致时,仅更新时间,而不是新增一条记录。
- 将时间和操作按钮合并到一行。
- 在首次进入剪贴板页面时,从
AppData
目录下读取history.json
文件,并将内容解析为剪贴板的历史内容,显示在界面上。 - 增加
ClipboardItems
映射到界面元素的异常处理,忽略无效的数据。
最终效果图#