傳統搜尋就像 Google,一次只看一個關鍵字;
但筆記往往有不同的脈絡。
舉個例子:
這時候,我意識到要讓筆記「有結構」,
需要兩種東西:
分類(Category)和標籤(Tags)。
分類是筆記的主體歸屬。
每篇筆記只能有一個分類,
它幫助我在「大方向」上找到想要的東西。
像是:
分類解決了「大方向」,
但同一個主題下的筆記,還可能橫跨多種議題。
例如我在「學習」分類下,可能有:
我在搜尋時只要輸入 Hook 或 AI,
不論在哪個分類都能快速找到相關內容。
import { useState, useEffect } from "react"
export default function NoteManager() {
const [notes, setNotes] = useState(() => {
const saved = localStorage.getItem("notes")
return saved ? JSON.parse(saved) : []
})
const [title, setTitle] = useState("")
const [content, setContent] = useState("")
const [category, setCategory] = useState("學習")
const [tags, setTags] = useState("")
const [search, setSearch] = useState("")
const [filter, setFilter] = useState("全部")
useEffect(() => {
localStorage.setItem("notes", JSON.stringify(notes))
}, [notes])
const addNote = () => {
if (!title.trim()) return
const newNote = {
id: Date.now(),
title,
content,
category,
tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
time: new Date().toLocaleString(),
}
setNotes([newNote, ...notes])
setTitle("")
setContent("")
setTags("")
}
const deleteNote = (id) => {
setNotes(notes.filter((n) => n.id !== id))
}
const categories = ["全部", "學習", "工作", "生活"]
const filtered = notes.filter((n) => {
const tags = Array.isArray(n.tags) ? n.tags : [] // 🧠 防止 undefined
const matchSearch =
n.title.toLowerCase().includes(search.toLowerCase()) ||
tags.some((t) => t.toLowerCase().includes(search.toLowerCase()))
const matchCategory = filter === "全部" || n.category === filter
return matchSearch && matchCategory
})
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">📒 筆記管理工具 - 升級版</h1>
<div className="flex space-x-2">
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="border p-2 rounded"
>
{categories.map((c) => (
<option key={c}>{c}</option>
))}
</select>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜尋標題或標籤..."
className="flex-1 border p-2 rounded"
/>
</div>
<div className="bg-white p-4 rounded shadow space-y-3">
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="筆記標題"
className="border w-full p-2 rounded"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="筆記內容"
className="border w-full p-2 rounded h-24"
/>
<div className="flex space-x-2">
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="border p-2 rounded"
>
{categories.slice(1).map((c) => (
<option key={c}>{c}</option>
))}
</select>
<input
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="輸入標籤(以逗號分隔)"
className="flex-1 border p-2 rounded"
/>
</div>
<button
onClick={addNote}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
新增筆記
</button>
</div>
<div className="grid md:grid-cols-2 gap-4">
{filtered.map((n) => (
<div key={n.id} className="bg-gray-50 border p-4 rounded shadow-sm">
<h2 className="text-lg font-bold">{n.title}</h2>
<p className="text-sm text-gray-600">{n.content}</p>
<div className="mt-2 flex flex-wrap gap-1 text-sm">
<span className="bg-green-200 px-2 py-0.5 rounded">{n.category}</span>
{Array.isArray(n.tags) &&
n.tags.map((t, i) => (
<span key={i} className="bg-yellow-200 px-2 py-0.5 rounded">
#{t}
</span>
))}
</div>
<div className="text-xs text-gray-500 mt-1">{n.time}</div>
<button
onClick={() => deleteNote(n.id)}
className="text-red-600 text-sm mt-2 hover:underline"
>
刪除
</button>
</div>
))}
</div>
</div>
)
}