增加了双向链接和全局搜索

This commit is contained in:
2026-02-25 09:20:38 +08:00
parent 317b9c7c26
commit ee3de1968c
12 changed files with 490 additions and 84 deletions
+146 -2
View File
@@ -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>