From f3f9919133573ecc5fb953bf5f7ac333c8a0e988 Mon Sep 17 00:00:00 2001 From: tabzzz Date: Fri, 24 Jan 2025 21:15:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(mini-markdown-editor):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=BC=96=E8=BE=91=E5=99=A8=E5=86=85=E5=AE=B9=E7=9A=84?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mini-markdown-editor/src/common/index.ts | 2 + .../src/components/Editor/index.tsx | 18 ++- .../src/components/Toolbar/CopyCodeButton.tsx | 42 +++++++ .../src/components/Toolbar/ToolbarItem.tsx | 6 +- .../src/components/Toolbar/index.tsx | 36 ++++-- .../mini-markdown-editor/src/store/editor.ts | 8 +- .../mini-markdown-editor/src/utils/storage.ts | 117 ++++++++++++++++++ 7 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 packages/mini-markdown-editor/src/common/index.ts create mode 100644 packages/mini-markdown-editor/src/components/Toolbar/CopyCodeButton.tsx create mode 100644 packages/mini-markdown-editor/src/utils/storage.ts diff --git a/packages/mini-markdown-editor/src/common/index.ts b/packages/mini-markdown-editor/src/common/index.ts new file mode 100644 index 0000000..1a15c7f --- /dev/null +++ b/packages/mini-markdown-editor/src/common/index.ts @@ -0,0 +1,2 @@ +// 编辑器内容KEY +export const EDITOR_CONTENT_KEY = "markdown-editor-content"; diff --git a/packages/mini-markdown-editor/src/components/Editor/index.tsx b/packages/mini-markdown-editor/src/components/Editor/index.tsx index b789a64..7091004 100644 --- a/packages/mini-markdown-editor/src/components/Editor/index.tsx +++ b/packages/mini-markdown-editor/src/components/Editor/index.tsx @@ -1,4 +1,4 @@ -import { FC, useRef } from "react"; +import { FC, useEffect, useRef } from "react"; import styled from "styled-components"; import CodeMirror, { type EditorView, ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; @@ -6,6 +6,8 @@ import { languages } from "@codemirror/language-data"; import * as events from "@uiw/codemirror-extensions-events"; import { useEditorContentStore } from "@/store/editor"; import { handleEditorScroll } from "@/utils/handle-scroll"; +import { safeLocalStorage } from "@/utils/storage"; +import { EDITOR_CONTENT_KEY } from "@/common"; const ScrollWrapper = styled.div` width: 100%; @@ -40,11 +42,23 @@ const Editor: FC = () => { setContent, scrollWrapper, setScrollWrapper, + editorView, setEditorView, previewView, setEditorRef, } = useEditorContentStore(); const editorRef = useRef(null); + const localStorage = safeLocalStorage(); + + // 处理重加载后的光标位置 + useEffect(() => { + if (editorView && content) { + // 将光标移动到文档末尾 + editorView.dispatch({ + selection: { anchor: content.length, head: content.length }, + }); + } + }, [editorView]); // 编辑器挂载完成后将编辑器示例存储起来 const handleCreate = (view: EditorView) => { @@ -54,6 +68,8 @@ const Editor: FC = () => { const handleChange = (val: string) => { setContent(val); + // 本地同步存储 + localStorage.setItem(EDITOR_CONTENT_KEY, val); }; const eventExt = events.scroll({ diff --git a/packages/mini-markdown-editor/src/components/Toolbar/CopyCodeButton.tsx b/packages/mini-markdown-editor/src/components/Toolbar/CopyCodeButton.tsx new file mode 100644 index 0000000..2e7ccf7 --- /dev/null +++ b/packages/mini-markdown-editor/src/components/Toolbar/CopyCodeButton.tsx @@ -0,0 +1,42 @@ +import { Button, message } from "antd"; +import { FC } from "react"; +import code from "@/mock/preview.md?raw"; + +interface CopyCodeButtonProps { + textToCopy?: string; + successMessage?: string; + errorMessage?: string; +} + +export const CopyCodeButton: FC = ({ + textToCopy = code, + successMessage = "已复制!", + errorMessage = "复制失败", +}) => { + // 仅在开发环境下展示 + if (process.env.NODE_ENV === "production") { + return null; + } + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(textToCopy); + message.success({ + content: successMessage, + duration: 1.5, + }); + } catch (err) { + console.error("复制失败:", err); + message.error({ + content: errorMessage, + duration: 2, + }); + } + }; + + return ( + + ); +}; diff --git a/packages/mini-markdown-editor/src/components/Toolbar/ToolbarItem.tsx b/packages/mini-markdown-editor/src/components/Toolbar/ToolbarItem.tsx index c78818f..08d065c 100644 --- a/packages/mini-markdown-editor/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/mini-markdown-editor/src/components/Toolbar/ToolbarItem.tsx @@ -20,8 +20,10 @@ const ToolbarItemWrapper = styled.div` background-color: #e6e6e6; } img { - width: 100%; - height: 100%; + width: 16px; + height: 16px; + display: block; + flex-shrink: 0; } `; diff --git a/packages/mini-markdown-editor/src/components/Toolbar/index.tsx b/packages/mini-markdown-editor/src/components/Toolbar/index.tsx index 3627327..5ca1e10 100644 --- a/packages/mini-markdown-editor/src/components/Toolbar/index.tsx +++ b/packages/mini-markdown-editor/src/components/Toolbar/index.tsx @@ -2,6 +2,7 @@ import { FC } from "react"; import styled from "styled-components"; import { useToolbar } from "@/hooks/use-toolbar"; import { ToolbarItem } from "./ToolbarItem"; +import { CopyCodeButton } from "./CopyCodeButton"; const ToolbarContent = styled.div` width: 100%; @@ -10,6 +11,17 @@ const ToolbarContent = styled.div` padding: 4px; display: flex; align-items: center; + justify-content: space-between; +`; + +const ToolbarLeft = styled.div` + display: flex; + align-items: center; +`; + +const ToolbarRight = styled.div` + display: flex; + align-items: center; `; const Divider = styled.div` @@ -27,13 +39,23 @@ const Toolbar: FC = () => { return ( - {toolbars.map((item, index) => - item.type === "line" ? ( - - ) : ( - - ), - )} + + {toolbars.map((item, index) => + item.type === "line" ? ( + + ) : ( + + ), + )} + + + + ); }; diff --git a/packages/mini-markdown-editor/src/store/editor.ts b/packages/mini-markdown-editor/src/store/editor.ts index 1584cda..53d93bd 100644 --- a/packages/mini-markdown-editor/src/store/editor.ts +++ b/packages/mini-markdown-editor/src/store/editor.ts @@ -1,7 +1,8 @@ import { create } from "zustand"; -import code from "@/mock/preview.md?raw"; import type { EditorView } from "@codemirror/view"; import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { safeLocalStorage } from "@/utils/storage"; +import { EDITOR_CONTENT_KEY } from "@/common"; interface EditorContentStoreType { content: string; @@ -19,9 +20,12 @@ interface EditorContentStoreType { setPreviewView: (view: HTMLElement | null) => void; } +const localStorage = safeLocalStorage(); + // 编辑器内容状态 const useEditorContentStore = create((set, get) => ({ - content: code, + // content: code, + content: localStorage.getItem(EDITOR_CONTENT_KEY) || "", setContent: (content: string) => set({ content }), scrollWrapper: "", setScrollWrapper: (scrollWrapper: string) => set({ scrollWrapper }), diff --git a/packages/mini-markdown-editor/src/utils/storage.ts b/packages/mini-markdown-editor/src/utils/storage.ts new file mode 100644 index 0000000..3a485b3 --- /dev/null +++ b/packages/mini-markdown-editor/src/utils/storage.ts @@ -0,0 +1,117 @@ +/** + * localStorage 包装器 + */ +export interface SafeStorage { + getItem: (key: string) => T | null; + setItem: (key: string, value: T) => void; + removeItem: (key: string) => void; + clear: () => void; +} + +export function safeLocalStorage(): SafeStorage { + //* 使用闭包存储storage状态 + const getStorage = (): Storage | null => { + try { + // 检查localStorage是否可用 + if (typeof window !== "undefined" && window.localStorage) { + const testKey = "__storage_test__"; + window.localStorage.setItem(testKey, testKey); + window.localStorage.removeItem(testKey); + return window.localStorage; + } + } catch (e) { + console.error("localStorage is not available:", e); + } + return null; + }; + + // 缓存storage实例 + const storage = getStorage(); + + // 统一的错误处理 + const handleStorageError = (operation: string, key?: string) => { + const message = key + ? `Attempted to ${operation} "${key}" ${operation === "get" ? "from" : "in"} localStorage, but localStorage is not available.` + : `Attempted to ${operation} localStorage, but localStorage is not available.`; + console.warn(message); + }; + + // 序列化和反序列化数据 + const serialize = (value: T): string => { + try { + return JSON.stringify(value); + } catch (e) { + console.error("Failed to serialize value:", e); + return String(value); + } + }; + const deserialize = (value: string): T | null => { + try { + return JSON.parse(value) as T; + } catch (e) { + console.error("Failed to deserialize value:", e); + return null; + } + }; + + return { + // 提取数据 + getItem(key: string): T | null { + if (!storage) { + handleStorageError("get", key); + return null; + } + + try { + const value = storage.getItem(key); + return value ? deserialize(value) : null; + } catch (e) { + console.error(`Error getting item "${key}" from localStorage:`, e); + return null; + } + }, + + // 存储数据 + setItem(key: string, value: T): void { + if (!storage) { + handleStorageError("set", key); + return; + } + + try { + storage.setItem(key, serialize(value)); + } catch (e) { + console.error(`Error setting item "${key}" in localStorage:`, e); + // TODO: 处理超额存储 + } + }, + + // 删除指定数据项 + removeItem(key: string): void { + if (!storage) { + handleStorageError("remove", key); + return; + } + + try { + storage.removeItem(key); + } catch (e) { + console.error(`Error removing item "${key}" from localStorage:`, e); + } + }, + + // 清空所有数据项 + clear(): void { + if (!storage) { + handleStorageError("clear"); + return; + } + + try { + storage.clear(); + } catch (e) { + console.error("Error clearing localStorage:", e); + } + }, + }; +} -- Gitee