Skip to content

Commit da02657

Browse files
committed
feat: add notes feature
1 parent d7ea299 commit da02657

29 files changed

+1409
-98
lines changed

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\"",
1010
"preview": "pnpm run build && vite preview",
1111
"pick": "tsx scripts/pick.ts",
12+
"sync-notes": "tsx scripts/sync-notes.ts",
1213
"prepare": "husky install",
1314
"predeploy": "pnpm run build",
1415
"deploy": "gh-pages -d dist"
1516
},
1617
"dependencies": {
1718
"@hookform/resolvers": "^3.10.0",
19+
"@radix-ui/react-alert-dialog": "^1.1.6",
1820
"@radix-ui/react-dialog": "^1.1.5",
1921
"@radix-ui/react-dropdown-menu": "^2.1.5",
2022
"@radix-ui/react-navigation-menu": "^1.2.4",
@@ -23,12 +25,15 @@
2325
"@tailwindcss/typography": "^0.5.16",
2426
"class-variance-authority": "^0.7.1",
2527
"clsx": "^2.1.1",
28+
"idb": "^8.0.2",
2629
"lucide-react": "^0.474.0",
2730
"motion": "^12.0.6",
31+
"nanoid": "^5.0.9",
2832
"react": "^18.3.1",
2933
"react-dom": "^18.3.1",
3034
"react-hook-form": "^7.54.2",
3135
"react-markdown": "^9.0.1",
36+
"react-markdown-editor-lite": "^1.3.4",
3237
"react-medium-image-zoom": "^5.2.13",
3338
"react-router-dom": "^7.1.1",
3439
"rehype-raw": "^7.0.0",

pnpm-lock.yaml

+239
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
62.4 KB
Loading
16.9 KB
Loading
14 KB
Loading
33.9 KB
Loading
11.8 KB
Loading

public/images/7f23c923.png

11.8 KB
Loading

public/notes/notes-data.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"id": "ac1YpriQAsqi6fOg-0QYd",
4+
"title": "新随记",
5+
"createdAt": "2025/2/9 14:42:30",
6+
"updatedAt": "2025/2/9 21:47:56"
7+
},
8+
{
9+
"id": "Go6ejUgYlygfuEhFtODLn",
10+
"title": "新随记1234",
11+
"createdAt": "2025/2/9 14:51:04",
12+
"updatedAt": "2025/2/9 21:48:26"
13+
}
14+
]

public/notes/新随记1234_Go6ejU.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<!-- title: 新随记1234 -->
2+
3+
请在这里输入随记内容...3215
4+
d2121

public/notes/新随记_ac1Ypr.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!-- title: 新随记 -->
2+
3+
请在这里输入随记内容...1234
4+
5+
4214¡
6+
7+
![image.png](1739083384079-ldwefl817f.png)
8+
9+
![image.png](1739083925268-1ptljqutoyf.png)

public/posts/React跨路由组件动画探索.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ TransitionGroup会监测其children的变化,将新的children与原有的chil
145145
146146
代码实现:
147147
148-
```javascript
148+
```js
149149
const container = document.querySelector(".flip-container");
150150
const btnAdd = document.querySelector("#add-btn");
151151
const btnDelete = document.querySelector("#delete-btn");

scripts/sync-notes.ts

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import fs from "fs";
2+
import path from "path";
3+
4+
interface Note {
5+
id: string;
6+
title: string;
7+
content?: string;
8+
createdAt: string;
9+
updatedAt: string;
10+
deleted?: boolean;
11+
synced?: boolean;
12+
}
13+
14+
interface ProcessedImage {
15+
fileName: string;
16+
base64: string;
17+
}
18+
19+
const NOTES_DIR = path.join(process.cwd(), "public/notes");
20+
const IMAGES_DIR = path.join(process.cwd(), "public/images");
21+
const NOTES_DATA_FILE = path.join(NOTES_DIR, "notes-data.json");
22+
23+
// 确保目录存在
24+
[NOTES_DIR, IMAGES_DIR].forEach((dir) => {
25+
if (!fs.existsSync(dir)) {
26+
fs.mkdirSync(dir, { recursive: true });
27+
}
28+
});
29+
30+
import { getImageHash, getImageExtFromBase64 } from "../src/utils/image";
31+
32+
// 处理 markdown 中的 base64 图片
33+
function processMarkdownImages(content: string): { content: string; images: ProcessedImage[] } {
34+
const images: ProcessedImage[] = [];
35+
let newContent = content;
36+
37+
// 匹配 markdown 中的图片
38+
const regex = /!\[([^\]]*)\]\(data:image\/[^;]+;base64,[^)]+\)/g;
39+
40+
newContent = content.replace(regex, (match, alt) => {
41+
const base64 = match.slice(match.indexOf("(") + 1, -1);
42+
const ext = getImageExtFromBase64(base64);
43+
const hash = getImageHash(base64);
44+
const fileName = `${hash}.${ext}`;
45+
images.push({
46+
fileName,
47+
base64,
48+
});
49+
return `![${alt}](${fileName})`;
50+
});
51+
52+
return { content: newContent, images };
53+
}
54+
55+
// 确保目录存在
56+
if (!fs.existsSync(NOTES_DIR)) {
57+
fs.mkdirSync(NOTES_DIR, { recursive: true });
58+
}
59+
60+
// 读取导出的JSON文件
61+
const exportFile = process.argv[2];
62+
if (!exportFile) {
63+
console.error("请提供导出的JSON文件路径");
64+
process.exit(1);
65+
}
66+
67+
const notes = JSON.parse(fs.readFileSync(exportFile, "utf-8"));
68+
69+
// 读取现有的 notes 数据
70+
let existingNotesData: Note[] = [];
71+
if (fs.existsSync(NOTES_DATA_FILE)) {
72+
existingNotesData = JSON.parse(fs.readFileSync(NOTES_DATA_FILE, "utf-8"));
73+
}
74+
75+
// 合并新旧数据
76+
const mergedNotesData = existingNotesData.slice();
77+
78+
// 更新或添加新的笔记至notes-data.json
79+
notes.forEach((note: Note) => {
80+
const existingIndex = mergedNotesData.findIndex((n) => n.id === note.id);
81+
const noteData = {
82+
id: note.id,
83+
title: note.title,
84+
createdAt: note.createdAt,
85+
updatedAt: note.updatedAt,
86+
};
87+
88+
if (note.deleted) {
89+
// 如果笔记被删除,从合并数据中移除
90+
if (existingIndex !== -1) {
91+
mergedNotesData.splice(existingIndex, 1);
92+
}
93+
} else {
94+
// 更新或添加笔记
95+
if (existingIndex !== -1) {
96+
mergedNotesData[existingIndex] = noteData;
97+
} else {
98+
mergedNotesData.push(noteData);
99+
}
100+
}
101+
});
102+
103+
// 写入更新后的数据
104+
fs.writeFileSync(NOTES_DATA_FILE, JSON.stringify(mergedNotesData, null, 2));
105+
106+
function sanitizeFileName(name: string): string {
107+
return name
108+
.replace(/[<>:"/\\|?*]/g, "") // 移除非法字符
109+
.replace(/\s+/g, "_") // 空格替换为下划线
110+
.slice(0, 50); // 限制长度
111+
}
112+
113+
// 获取现有的markdown文件列表
114+
const existingFiles = fs.readdirSync(NOTES_DIR).filter((file) => file.endsWith(".md"));
115+
116+
// 将每个note内容写入单独的markdown文件
117+
notes.forEach((note: Note) => {
118+
const shortId = note.id.substring(0, 6);
119+
const safeTitle = sanitizeFileName(note.title);
120+
const fileName = `${safeTitle}_${shortId}.md`;
121+
const filePath = path.join(NOTES_DIR, fileName);
122+
123+
if (note.deleted) {
124+
// 如果笔记已被删除,删除对应的markdown文件
125+
if (fs.existsSync(filePath)) {
126+
fs.unlinkSync(filePath);
127+
console.log(`删除文件: ${fileName}`);
128+
}
129+
} else {
130+
// 处理笔记内容中的图片
131+
const { content: processedContent, images } = processMarkdownImages(note.content || "");
132+
133+
// 保存图片文件
134+
images.forEach(({ fileName, base64 }) => {
135+
const imagePath = path.join(IMAGES_DIR, fileName);
136+
// 如果文件已存在,跳过
137+
if (fs.existsSync(imagePath)) {
138+
console.log(`图片已存在,跳过: ${fileName}`);
139+
return;
140+
}
141+
const imageData = base64.split(",")[1];
142+
fs.writeFileSync(imagePath, Buffer.from(imageData, "base64"));
143+
console.log(`写入图片: ${fileName}`);
144+
});
145+
146+
// 写入处理后的 markdown 文件
147+
fs.writeFileSync(filePath, processedContent);
148+
console.log(`写入文件: ${fileName}`);
149+
}
150+
});
151+
152+
// 清理已不存在的笔记对应的文件
153+
const validFileNames = mergedNotesData.map(
154+
(note: any) => `${sanitizeFileName(note.title)}_${note.id.substring(0, 6)}.md`
155+
);
156+
157+
existingFiles.forEach((file) => {
158+
if (!validFileNames.includes(file) && file !== "notes-data.json") {
159+
const filePath = path.join(NOTES_DIR, file);
160+
fs.unlinkSync(filePath);
161+
console.log(`删除旧文件: ${file}`);
162+
}
163+
});
164+
165+
console.log("同步完成!");

src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { HashRouter, Routes, Route } from "react-router-dom";
66
import BlogPost from "./views/BlogPost";
77
import BlogList from "./views/BlogList";
88
import WorkSpace from "./views/WorkSpace";
9+
import Notes from "./views/Notes";
910

1011
function App() {
1112
return (
@@ -16,6 +17,7 @@ function App() {
1617
<Route path="/" element={<BlogList />} />
1718
<Route path="/post/:id" element={<BlogPost />} />
1819
<Route path="/workspace" element={<WorkSpace />} />
20+
<Route path="/notes" element={<Notes />} />
1921
</Routes>
2022
</Layout>
2123
<Toaster />

src/components/CodeBlock.tsx

+13-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from "react";
1+
import React, { memo, useEffect, useRef, useState } from "react";
22
import { Copy, ChevronDown, ChevronUp, ThumbsUp } from "lucide-react";
33
import { Button } from "@/components/ui/button";
44
import { Loading } from "@/components/ui/loading";
@@ -9,22 +9,23 @@ import { useTheme } from "../ThemeProvider";
99

1010
interface CodeBlockProps extends React.HTMLAttributes<HTMLElement> {
1111
language: string;
12-
children: React.ReactNode;
12+
codeText: string;
13+
showParseLoading?: boolean;
1314
}
1415

15-
const CodeBlock = ({ language, children, ...restProps }: CodeBlockProps) => {
16+
const CodeBlock = ({ language, codeText, showParseLoading = true, ...restProps }: CodeBlockProps) => {
1617
const codeRef = useRef<HTMLDivElement>(null);
1718
const [isCollapsed, setIsCollapsed] = useState(false);
1819
const [isCopied, setIsCopied] = useState(false);
19-
const [isCodePasing, setIsCodePasing] = useState(false);
20+
const [isCodeParsing, setIsCodeParsing] = useState(true);
2021

2122
const { toast } = useToast();
2223
const { theme } = useTheme();
2324

2425
useEffect(() => {
25-
const code = codeRef.current?.textContent || "";
26-
setIsCodePasing(true);
27-
codeToHtml(code.trim(), {
26+
const code = codeText.trim();
27+
setIsCodeParsing(true);
28+
codeToHtml(code, {
2829
lang: language,
2930
themes: {
3031
dark: "github-dark-high-contrast",
@@ -41,9 +42,9 @@ const CodeBlock = ({ language, children, ...restProps }: CodeBlockProps) => {
4142
codeRef.current.innerHTML = html;
4243
})
4344
.finally(() => {
44-
setIsCodePasing(false);
45+
setIsCodeParsing(false);
4546
});
46-
}, [theme]);
47+
}, [theme, language, codeText]);
4748

4849
const copyToClipboard = async () => {
4950
try {
@@ -61,14 +62,6 @@ const CodeBlock = ({ language, children, ...restProps }: CodeBlockProps) => {
6162
}
6263
};
6364

64-
const mergedProps = {
65-
...restProps,
66-
style: {
67-
...restProps.style,
68-
display: isCodePasing ? "none" : "block",
69-
},
70-
};
71-
7265
return (
7366
<div className="relative group code-block-wrapper">
7467
<div className="w-full pl-4 pr-4 rounded-t-sm flex h-10 dark:bg-zinc-900 bg-zinc-100 items-center gap-2 group-hover:opacity-100 transition-opacity">
@@ -90,13 +83,11 @@ const CodeBlock = ({ language, children, ...restProps }: CodeBlockProps) => {
9083
</div>
9184
</div>
9285
<div className={cn("transition-all duration-200 overflow-auto", isCollapsed ? "max-h-0" : "max-h-[70vh]")}>
93-
{isCodePasing && <Loading description="loading..." className="h-40" />}
94-
<div ref={codeRef} {...mergedProps}>
95-
{children}
96-
</div>
86+
{isCodeParsing && showParseLoading && <Loading description="loading..." className="h-40" />}
87+
<div ref={codeRef} {...restProps}></div>
9788
</div>
9889
</div>
9990
);
10091
};
10192

102-
export default CodeBlock;
93+
export default memo(CodeBlock);

src/components/Layout.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface LayoutProps {
77

88
export function Layout({ children }: LayoutProps) {
99
return (
10-
<div className="min-h-screen bg-background">
10+
<div className="min-h-screen bg-background flex flex-col">
1111
<header className="sticky top-0 z-40 bg-background border-b">
1212
<div className="container mx-auto px-4 py-4 flex items-center">
1313
<div className="flex items-center md:absolute left-8">
@@ -21,7 +21,7 @@ export function Layout({ children }: LayoutProps) {
2121
</div>
2222
</div>
2323
</header>
24-
<main className="container mx-auto px-4 md:px-8 lg:px-32 xl:px-64 py-8">{children}</main>
24+
<main className="main-page flex-1">{children}</main>
2525
</div>
2626
);
2727
}

0 commit comments

Comments
 (0)