"use client"; import Link from "next/link"; import { Fragment, ReactNode, useEffect, useMemo, useRef, useState } from "react"; import data from "../data/content-templates.json"; import styles from "./MarkdownEditor.module.css"; type ViewMode = "edit" | "split" | "preview"; type DocumentItem = (typeof data.documents)[number]; export default function MarkdownEditorPage() { const [documents, setDocuments] = useState(data.documents); const [activeId, setActiveId] = useState(data.documents[0].id); const [contents, setContents] = useState>(() => Object.fromEntries(data.documents.map((doc) => [doc.id, doc.content]))); const [view, setView] = useState("split"); const [query, setQuery] = useState(""); const [saved, setSaved] = useState(true); const [toast, setToast] = useState(""); const textareaRef = useRef(null); useEffect(() => { try { const cached = window.localStorage.getItem("orbit-markdown-workspace"); if (cached) { const parsed = JSON.parse(cached); if (parsed.contents) setContents((current) => ({ ...current, ...parsed.contents })); if (Array.isArray(parsed.documents)) setDocuments(parsed.documents); if (typeof parsed.activeId === "string") setActiveId(parsed.activeId); } } catch { /* use JSON defaults */ } }, []); useEffect(() => { setSaved(false); const timer = window.setTimeout(() => { window.localStorage.setItem("orbit-markdown-workspace", JSON.stringify({ documents, contents, activeId })); setSaved(true); }, 500); return () => window.clearTimeout(timer); }, [documents, contents, activeId]); const activeDocument = documents.find((doc) => doc.id === activeId) ?? documents[0]; const content = contents[activeId] ?? ""; const filteredDocuments = documents.filter((doc) => doc.title.toLowerCase().includes(query.toLowerCase())); const stats = useMemo(() => ({ chars: content.replace(/\s/g, "").length, lines: content.split("\n").length, words: content.trim() ? content.trim().split(/\s+/).length : 0, }), [content]); const notify = (message: string) => { setToast(message); window.setTimeout(() => setToast(""), 2000); }; const updateContent = (value: string) => setContents((current) => ({ ...current, [activeId]: value })); const insert = (before: string, placeholder = "文本", after = "") => { const textarea = textareaRef.current; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selected = content.slice(start, end) || placeholder; const next = `${content.slice(0, start)}${before}${selected}${after}${content.slice(end)}`; updateContent(next); window.requestAnimationFrame(() => { textarea.focus(); textarea.setSelectionRange(start + before.length, start + before.length + selected.length); }); }; const createDocument = () => { const id = `md-${Date.now()}`; const newDocument: DocumentItem = { id, title: "未命名文档", category: "草稿", updated: "刚刚", content: "# 未命名文档\n\n开始写作……" }; setDocuments((current) => [newDocument, ...current]); setContents((current) => ({ ...current, [id]: newDocument.content })); setActiveId(id); notify("已创建新文档"); }; const renameDocument = (title: string) => setDocuments((current) => current.map((doc) => doc.id === activeId ? { ...doc, title, updated: "刚刚" } : doc)); const downloadDocument = () => { const blob = new Blob([content], { type: "text/markdown;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = window.document.createElement("a"); link.href = url; link.download = `${activeDocument?.title || "document"}.md`; link.click(); URL.revokeObjectURL(url); notify("Markdown 文件已导出"); }; return (
M↓Paper
{saved ? "已自动保存" : "正在保存…"}
{activeDocument?.category || "文档"} renameDocument(event.target.value)} aria-label="文档标题" />
Markdown · UTF-8
{view !== "preview" &&
{content.split("\n").map((_, index) => {index + 1})}