增加了双向链接和全局搜索
This commit is contained in:
+146
-2
@@ -3,7 +3,7 @@
|
||||
import { SidebarContent, ResizableSidebar } from "@/components/sidebar";
|
||||
import { Editor } from "@/components/editor";
|
||||
import { useEditorStore } from "@/lib/store";
|
||||
import { Hash, X, Plus, ChevronLeft, ChevronRight, Sparkles, Lock, Unlock, FolderOpen } from "lucide-react";
|
||||
import { Hash, X, Plus, ChevronLeft, ChevronRight, Sparkles, Lock, Unlock, FolderOpen, History, RotateCcw } from "lucide-react";
|
||||
import { useSettingsStore } from "@/lib/settings-store";
|
||||
import { exportPageAsMarkdown } from "@/lib/export";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -12,6 +12,8 @@ import { useSearchStore } from "@/lib/search-store";
|
||||
import { type Editor as TiptapEditor } from "@tiptap/react";
|
||||
import { ImportProvider } from "@/components/import-context";
|
||||
import { AIChatPanel } from "@/components/chat/ai-chat-panel";
|
||||
import { getBacklinks, getOutgoingLinks, getUnresolvedLinkTitles } from "@/lib/wiki-links";
|
||||
import { getPageHistory } from "@/lib/page-history";
|
||||
|
||||
const ICONS = [
|
||||
"📄", "📝", "📒", "📘", "📙", "📕", "📗", "📚",
|
||||
@@ -22,7 +24,7 @@ const ICONS = [
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const { activePageId, pages, updatePage } = useEditorStore();
|
||||
const { activePageId, pages, updatePage, setActivePageId, addPage } = useEditorStore();
|
||||
const { openSearchWithTag } = useSearchStore();
|
||||
const { timezone } = useSettingsStore();
|
||||
|
||||
@@ -47,6 +49,26 @@ export default function Home() {
|
||||
return result;
|
||||
}, [activePage, pages]);
|
||||
|
||||
const outgoingLinks = useMemo(() => {
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getOutgoingLinks(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
|
||||
const backlinks = useMemo(() => {
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getBacklinks(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
|
||||
const unresolvedLinks = useMemo(() => {
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getUnresolvedLinkTitles(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
|
||||
const localHistory = useMemo(() => {
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getPageHistory(activePage.id);
|
||||
}, [activePage]);
|
||||
|
||||
const addTag = () => {
|
||||
if (!activePage) return;
|
||||
const next = tagInput.trim();
|
||||
@@ -57,6 +79,17 @@ export default function Home() {
|
||||
setIsAddingTag(false);
|
||||
};
|
||||
|
||||
const handleOpenWikiLink = async (title: string) => {
|
||||
const target = pages.find((p) => p.type === "file" && (p.title || "").trim() === title);
|
||||
if (target) {
|
||||
setActivePageId(target.id);
|
||||
return;
|
||||
}
|
||||
const shouldCreate = window.confirm(`未找到页面“${title}”。是否立即创建?`);
|
||||
if (!shouldCreate) return;
|
||||
await addPage(null, "file", { title, content: "" });
|
||||
};
|
||||
|
||||
return (
|
||||
<ImportProvider>
|
||||
<div className="relative flex h-[100dvh] w-full overflow-hidden bg-background">
|
||||
@@ -250,8 +283,119 @@ export default function Home() {
|
||||
onEditorReady={setEditor}
|
||||
onToggleAI={() => setIsChatOpen(!isChatOpen)}
|
||||
onExport={() => exportPageAsMarkdown(activePage)}
|
||||
onOpenWikiLink={handleOpenWikiLink}
|
||||
editable={!activePage.isLocked}
|
||||
/>
|
||||
|
||||
<section className="ui-enter mt-8 space-y-4">
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="text-sm font-semibold text-foreground">关联页面</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">在正文中使用 `[[页面名]]` 自动建立链接关系,支持 `Ctrl/Cmd + 点击` 跳转。</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{outgoingLinks.length > 0 ? (
|
||||
outgoingLinks.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActivePageId(item.id)}
|
||||
className="rounded-md border border-border/70 bg-muted/40 px-2.5 py-1 text-xs text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{item.icon ? `${item.icon} ` : ""}
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">暂无关联页面</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="text-sm font-semibold text-foreground">反向链接</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">这些页面提到了当前页面。</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{backlinks.length > 0 ? (
|
||||
backlinks.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActivePageId(item.id)}
|
||||
className="rounded-md border border-border/70 bg-muted/40 px-2.5 py-1 text-xs text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{item.icon ? `${item.icon} ` : ""}
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">暂无反向链接</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-4">
|
||||
<h3 className="text-sm font-semibold text-foreground">未解析链接</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">这些 `[[页面名]]` 还没有对应页面,可一键创建。</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{unresolvedLinks.length > 0 ? (
|
||||
unresolvedLinks.map((title) => (
|
||||
<button
|
||||
key={title}
|
||||
onClick={async () => {
|
||||
await addPage(null, "file", { title, content: "" });
|
||||
}}
|
||||
className="rounded-md border border-dashed border-border bg-muted/30 px-2.5 py-1 text-xs text-foreground transition-colors hover:bg-accent"
|
||||
title={`创建页面:${title}`}
|
||||
>
|
||||
+ {title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">全部链接已解析</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<History size={14} className="text-muted-foreground" />
|
||||
<h3 className="text-sm font-semibold text-foreground">本地历史版本</h3>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">自动保存最近修改快照(当前浏览器本地)。</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{localHistory.length > 0 ? (
|
||||
localHistory.slice(0, 8).map((snapshot, index) => (
|
||||
<div key={`${snapshot.timestamp}-${index}`} className="flex items-center justify-between rounded-md border border-border/60 bg-muted/20 px-2.5 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-xs text-foreground">
|
||||
{snapshot.title || "无标题"}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{new Date(snapshot.timestamp).toLocaleString("zh-CN", {
|
||||
timeZone: timezone || "Asia/Shanghai",
|
||||
hour12: false,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const ok = window.confirm("恢复该历史版本后将覆盖当前内容,是否继续?");
|
||||
if (!ok || !activePage) return;
|
||||
await updatePage(activePage.id, {
|
||||
title: snapshot.title,
|
||||
content: snapshot.content,
|
||||
});
|
||||
}}
|
||||
className="ui-btn-secondary px-2 py-1 text-xs"
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
恢复
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">暂无历史记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user