"use client" import { useMemo, useState } from "react" import { toast } from "sonner" import { Plus, Search, Boxes, X, Settings, Sparkles } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Badge } from "@/components/ui/badge" import type { KnowledgeItem, KnowledgeItemInput } from "@/lib/types" import { CATEGORIES, MASTERY_LEVELS, MASTERY_META } from "@/lib/types" import { createItem, deleteItem, reviewItem, toggleFavorite, updateItem, } from "@/lib/knowledge" import { StatsBar } from "@/components/stats-bar" import { KnowledgeCard } from "@/components/knowledge-card" import { ItemDialog } from "@/components/item-dialog" import { DetailDialog } from "@/components/detail-dialog" import { AISettingsDialog } from "@/components/ai/ai-settings-dialog" import { AIBatchImportDialog } from "@/components/ai/ai-batch-import-dialog" type SortKey = "recent" | "review" | "title" type SelectOption = { value: T; label: string } // 通过对象定义 value 与展示文案,下方循环渲染,便于维护 const CATEGORY_OPTIONS: SelectOption[] = [ { value: "all", label: "全部分类" }, ...CATEGORIES.map((c) => ({ value: c, label: c })), ] const MASTERY_OPTIONS: SelectOption[] = [ { value: "all", label: "全部状态" }, ...MASTERY_LEVELS.map((m) => ({ value: m, label: MASTERY_META[m].label })), ] const SORT_OPTIONS: SelectOption[] = [ { value: "recent", label: "最近添加" }, { value: "review", label: "待复习" }, { value: "title", label: "按标题" }, ] export function KnowledgeDashboard({ initialItems }: { initialItems: KnowledgeItem[] }) { const [items, setItems] = useState(initialItems) const [query, setQuery] = useState("") const [category, setCategory] = useState("all") const [mastery, setMastery] = useState("all") const [favoritesOnly, setFavoritesOnly] = useState(false) const [sort, setSort] = useState("recent") const [activeTag, setActiveTag] = useState(null) const [formOpen, setFormOpen] = useState(false) const [editing, setEditing] = useState(null) const [detail, setDetail] = useState(null) const [settingsOpen, setSettingsOpen] = useState(false) const [batchOpen, setBatchOpen] = useState(false) const allTags = useMemo(() => { const map = new Map() for (const it of items) for (const t of it.tags) map.set(t, (map.get(t) ?? 0) + 1) return [...map.entries()].sort((a, b) => b[1] - a[1]) }, [items]) const filtered = useMemo(() => { let list = items.filter((it) => { if (category !== "all" && it.category !== category) return false if (mastery !== "all" && it.mastery !== mastery) return false if (favoritesOnly && !it.is_favorite) return false if (activeTag && !it.tags.includes(activeTag)) return false if (query) { const q = query.toLowerCase() const hay = [it.title, it.summary, it.content, it.notes, it.tags.join(" ")] .filter(Boolean) .join(" ") .toLowerCase() if (!hay.includes(q)) return false } return true }) list = [...list].sort((a, b) => { if (sort === "title") return a.title.localeCompare(b.title, "zh") if (sort === "review") { const av = a.next_review_at ? new Date(a.next_review_at).getTime() : Infinity const bv = b.next_review_at ? new Date(b.next_review_at).getTime() : Infinity return av - bv } return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() }) return list }, [items, category, mastery, favoritesOnly, activeTag, query, sort]) function openCreate() { setEditing(null) setFormOpen(true) } function openEdit(item: KnowledgeItem) { setEditing(item) setDetail(null) setFormOpen(true) } async function handleSubmit(input: KnowledgeItemInput) { try { if (editing) { const updated = await updateItem(editing.id, input) setItems((prev) => prev.map((it) => (it.id === updated.id ? updated : it))) toast.success("知识点已更新") } else { const created = await createItem(input) setItems((prev) => [created, ...prev]) toast.success("知识点已添加") } setFormOpen(false) setEditing(null) } catch (e) { toast.error("操作失败,请重试") console.log("[v0] submit error:", e) } } function handleBatchInserted(created: KnowledgeItem[]) { setItems((prev) => [...created, ...prev]) } async function handleDelete(id: string) { try { await deleteItem(id) setItems((prev) => prev.filter((it) => it.id !== id)) setDetail(null) toast.success("已删除") } catch (e) { toast.error("删除失败") console.log("[v0] delete error:", e) } } async function handleToggleFav(item: KnowledgeItem) { const value = !item.is_favorite setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, is_favorite: value } : it))) try { await toggleFavorite(item.id, value) } catch (e) { setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, is_favorite: !value } : it))) toast.error("收藏失败") console.log("[v0] fav error:", e) } } async function handleReview(item: KnowledgeItem) { try { const updated = await reviewItem(item.id, item.review_count, item.mastery) setItems((prev) => prev.map((it) => (it.id === updated.id ? updated : it))) setDetail((d) => (d && d.id === updated.id ? updated : d)) toast.success(`已复习 · 累计 ${updated.review_count} 次`) } catch (e) { toast.error("记录复习失败") console.log("[v0] review error:", e) } } const hasFilters = category !== "all" || mastery !== "all" || favoritesOnly || activeTag || query return (
{/* Header */}

前端知识库

DevVault · 收集 · 整理 · 复习

{/* Toolbar */}
setQuery(e.target.value)} placeholder="搜索标题、内容、标签..." className="pl-9" />
{/* Tag cloud */} {allTags.length > 0 && (
{allTags.slice(0, 16).map(([tag, count]) => ( ))} {hasFilters && ( )}
)}
{/* Grid */}

{filtered.length} / {items.length} 条记录

{filtered.length === 0 ? (

{items.length === 0 ? "还没有知识点,点击「新增」开始收集吧" : "没有匹配的结果"}

) : (
{filtered.map((item) => ( setDetail(item)} onToggleFav={() => handleToggleFav(item)} onTagClick={(t) => setActiveTag((cur) => (cur === t ? null : t))} /> ))}
)}
!o && setDetail(null)} onEdit={openEdit} onDelete={handleDelete} onReview={handleReview} onToggleFav={handleToggleFav} onItemUpdated={(updated) => { setItems((prev) => prev.map((it) => (it.id === updated.id ? updated : it))) setDetail((d) => (d && d.id === updated.id ? updated : d)) }} />
) }