性能优化
This commit is contained in:
@@ -18,13 +18,28 @@ export async function GET(request: NextRequest) {
|
||||
// 默认行为:返回所有页面(向后兼容)
|
||||
const usePagination = limit > 0 && limit <= 1000 && page > 0;
|
||||
|
||||
const baseSelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
content: !lightweight,
|
||||
tags: true,
|
||||
icon: true,
|
||||
type: true,
|
||||
parentId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
order: true,
|
||||
isLocked: true,
|
||||
} as const;
|
||||
|
||||
if (!usePagination) {
|
||||
const pages = await prisma.page.findMany({
|
||||
select: baseSelect,
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
const parsedPages = pages.map((p) => ({
|
||||
...p,
|
||||
content: lightweight ? '' : p.content,
|
||||
content: lightweight ? '' : (p.content || ''),
|
||||
tags: safeParseTags(p.tags),
|
||||
}));
|
||||
return NextResponse.json(parsedPages);
|
||||
@@ -34,6 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
const skip = (page - 1) * limit;
|
||||
const [pages, total] = await Promise.all([
|
||||
prisma.page.findMany({
|
||||
select: baseSelect,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
@@ -133,4 +149,3 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+6
-1
@@ -360,13 +360,18 @@ ul[data-type="taskList"],
|
||||
The component handles its own layout. */
|
||||
|
||||
/* Override Highlight.js background for a softer look */
|
||||
.ProseMirror pre {
|
||||
.ProseMirror pre {
|
||||
background: hsl(var(--code-bg)) !important;
|
||||
border: 1px solid hsl(var(--border) / 0.5);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin: 0.95rem 0;
|
||||
overflow-x: auto;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.ProseMirror pre code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
|
||||
+129
-74
@@ -25,7 +25,16 @@ const ICONS = [
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const { activePageId, pages, pageSync, updatePage, setActivePageId, addPage, retryPageSync, retryPendingSyncs, flushPendingSyncs } = useEditorStore();
|
||||
const activePageId = useEditorStore((s) => s.activePageId);
|
||||
const pages = useEditorStore((s) => s.pages);
|
||||
const pageSync = useEditorStore((s) => s.pageSync);
|
||||
const updatePage = useEditorStore((s) => s.updatePage);
|
||||
const setActivePageId = useEditorStore((s) => s.setActivePageId);
|
||||
const addPage = useEditorStore((s) => s.addPage);
|
||||
const retryPageSync = useEditorStore((s) => s.retryPageSync);
|
||||
const retryPendingSyncs = useEditorStore((s) => s.retryPendingSyncs);
|
||||
const flushPendingSyncs = useEditorStore((s) => s.flushPendingSyncs);
|
||||
const fetchPageContent = useEditorStore((s) => s.fetchPageContent);
|
||||
const { openSearchWithTag } = useSearchStore();
|
||||
const { timezone } = useSettingsStore();
|
||||
|
||||
@@ -36,6 +45,7 @@ export default function Home() {
|
||||
const [isAddingTag, setIsAddingTag] = useState(false);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [isIconPickerOpen, setIsIconPickerOpen] = useState(false);
|
||||
const [isLinksOpen, setIsLinksOpen] = useState(false);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(() => !activePageId);
|
||||
@@ -57,19 +67,22 @@ export default function Home() {
|
||||
}, [activePage, pages]);
|
||||
|
||||
const outgoingLinks = useMemo(() => {
|
||||
if (!isLinksOpen) return [];
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getOutgoingLinks(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
}, [activePage, pages, isLinksOpen]);
|
||||
|
||||
const backlinks = useMemo(() => {
|
||||
if (!isLinksOpen) return [];
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getBacklinks(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
}, [activePage, pages, isLinksOpen]);
|
||||
|
||||
const unresolvedLinks = useMemo(() => {
|
||||
if (!isLinksOpen) return [];
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getUnresolvedLinkTitles(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
}, [activePage, pages, isLinksOpen]);
|
||||
|
||||
const localHistory = useMemo(() => {
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
@@ -125,6 +138,27 @@ export default function Home() {
|
||||
};
|
||||
}, [flushPendingSyncs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePageId) return;
|
||||
const current = pages.find((p) => p.id === activePageId);
|
||||
if (!current || current.type === "folder") return;
|
||||
if (current.contentLoaded) return;
|
||||
fetchPageContent(activePageId);
|
||||
}, [activePageId, pages, fetchPageContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLinksOpen) return;
|
||||
const batchIds = pages
|
||||
.filter((p) => p.type === "file" && !p.contentLoaded)
|
||||
.slice(0, 8)
|
||||
.map((p) => p.id);
|
||||
if (batchIds.length === 0) return;
|
||||
|
||||
Promise.all(batchIds.map((id) => fetchPageContent(id))).catch((e) => {
|
||||
console.error("Failed to batch load pages for link analysis", e);
|
||||
});
|
||||
}, [isLinksOpen, pages, fetchPageContent]);
|
||||
|
||||
const handleEdgeSwipeStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (isMobileSidebarOpen) return;
|
||||
const touch = e.touches[0];
|
||||
@@ -411,80 +445,101 @@ export default function Home() {
|
||||
/>
|
||||
|
||||
<section className="ui-enter mt-6 space-y-3 md:mt-7">
|
||||
<div className="grid gap-2 md:grid-cols-3">
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">关联页面</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{outgoingLinks.length}</span>
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<button
|
||||
onClick={() => setIsLinksOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between rounded-md px-1 py-1 text-left transition-colors hover:bg-muted/45"
|
||||
aria-expanded={isLinksOpen}
|
||||
aria-label="切换链接分析面板"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">链接分析</h3>
|
||||
{isLinksOpen && (
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||
{outgoingLinks.length + backlinks.length + unresolvedLinks.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">正文写 `[[页面名]]` 可自动关联。</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{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 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{item.icon ? `${item.icon} ` : ""}
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground">暂无</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown size={15} className={cn("text-muted-foreground transition-transform", isLinksOpen && "rotate-180")} />
|
||||
</button>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">展开后计算关联页面、反向链接和未解析链接。</p>
|
||||
{isLinksOpen && (
|
||||
<div className="mt-2 grid gap-2 md:grid-cols-3">
|
||||
<div className="rounded-md border border-border/60 bg-muted/10 p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">关联页面</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{outgoingLinks.length}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">正文写 `[[页面名]]` 可自动关联。</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{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 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{item.icon ? `${item.icon} ` : ""}
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground">暂无</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">反向链接</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{backlinks.length}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">被哪些页面引用。</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{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 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{item.icon ? `${item.icon} ` : ""}
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground">暂无</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border/60 bg-muted/10 p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">反向链接</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{backlinks.length}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">被哪些页面引用。</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{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 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{item.icon ? `${item.icon} ` : ""}
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground">暂无</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">未解析链接</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{unresolvedLinks.length}</span>
|
||||
<div className="rounded-md border border-border/60 bg-muted/10 p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">未解析链接</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{unresolvedLinks.length}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">可一键创建缺失页面。</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{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 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
||||
title={`创建页面:${title}`}
|
||||
>
|
||||
+ {title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground">全部已解析</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">可一键创建缺失页面。</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{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 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
||||
title={`创建页面:${title}`}
|
||||
>
|
||||
+ {title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground">全部已解析</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
|
||||
Reference in New Issue
Block a user