"use client"; import { useSearchStore } from "@/lib/search-store"; import { Fragment, useEffect, useMemo, useState } from "react"; import { useEditorStore } from "@/lib/store"; import { Command } from "cmdk"; import { Search, FileText, Folder } from "lucide-react"; import { filterPagesByQuery, parseSearchQuery } from "@/lib/search-query"; const RECENT_PAGE_IDS_KEY = "noteai-recent-page-ids"; const MAX_RECENT_PAGES = 30; function escapeRegExp(input: string): string { return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function highlightText(text: string, terms: string[]) { if (!text || terms.length === 0) return text; const normalized = Array.from(new Set(terms.filter(Boolean).map((t) => t.toLowerCase()))); if (normalized.length === 0) return text; const pattern = normalized.map(escapeRegExp).join("|"); const regex = new RegExp(`(${pattern})`, "ig"); const parts = text.split(regex); return parts.map((part, idx) => { const matched = normalized.includes(part.toLowerCase()); return matched ? ( {part} ) : ( {part} ); }); } export function SearchCommand() { const { isOpen, setOpen, query, setQuery } = useSearchStore(); const { pages, setActivePageId } = useEditorStore(); const [recentPageIds, setRecentPageIds] = useState(() => { if (typeof window === "undefined") return []; try { const raw = localStorage.getItem(RECENT_PAGE_IDS_KEY); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string") : []; } catch { return []; } }); const parsedQuery = useMemo(() => parseSearchQuery(query), [query]); const filteredPages = useMemo(() => { const items = filterPagesByQuery(pages, query); const recentRank = new Map(recentPageIds.map((id, idx) => [id, idx])); return [...items].sort((a, b) => { const ra = recentRank.has(a.id) ? (recentRank.get(a.id) as number) : Number.MAX_SAFE_INTEGER; const rb = recentRank.has(b.id) ? (recentRank.get(b.id) as number) : Number.MAX_SAFE_INTEGER; if (ra !== rb) return ra - rb; const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0; const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0; return tb - ta; }); }, [pages, query, recentPageIds]); useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen(!isOpen); } }; document.addEventListener("keydown", down); return () => document.removeEventListener("keydown", down); }, [isOpen, setOpen]); const runCommand = (command: () => void) => { setOpen(false); command(); }; const rememberVisitedPage = (pageId: string) => { setRecentPageIds((prev) => { const next = [pageId, ...prev.filter((id) => id !== pageId)].slice(0, MAX_RECENT_PAGES); try { localStorage.setItem(RECENT_PAGE_IDS_KEY, JSON.stringify(next)); } catch { // ignore storage failures } return next; }); }; if (!isOpen) return null; return (
setOpen(false)} />
语法:`tag:标签` `type:file|folder` `updated:7d`
未找到匹配结果 {filteredPages.map((page) => ( runCommand(() => { setActivePageId(page.id); rememberVisitedPage(page.id); }) } className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground" > {page.type === "folder" ? : } {highlightText(page.title || "无标题", parsedQuery.textTerms)} {page.tags && page.tags.length > 0 && ( {page.tags.slice(0, 2).join(" · ")} )} ))}
); }