studynote/src/db/index.ts
MikiVL b02f23729b 修复笔记侧边栏多个 bug
- 移除管理员面板入口(UserMenu 中的按钮和相关 import)
- 修复重复文件夹问题:seed 判断同时检查 notes 和 folders 数量
- 文件夹新增 hover 删除按钮,附带确认弹窗
- 文件夹区域添加 overflow-y-auto 和 maxHeight 限制,防止过多文件夹遮挡笔记列表

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-26 09:02:27 +08:00

169 lines
5.2 KiB
TypeScript

import Dexie, { type Table } from 'dexie'
export interface Note {
id: string
title: string
content: string
folderId: string | null
tags: string[]
starred: boolean
createdAt: number
updatedAt: number
wordCount: number
deletedAt: number | null
}
export interface Folder {
id: string
name: string
parentId: string | null
order: number
createdAt: number
}
export interface Tag {
id: string
name: string
color: string
}
class NotesDB extends Dexie {
notes!: Table<Note>
folders!: Table<Folder>
tags!: Table<Tag>
constructor() {
super('notesapp')
this.version(1).stores({
notes: 'id, folderId, starred, updatedAt, createdAt, *tags',
folders: 'id, parentId, order',
tags: 'id, name',
})
this.version(2).stores({
notes: 'id, folderId, starred, updatedAt, createdAt, deletedAt, *tags',
folders: 'id, parentId, order',
tags: 'id, name',
}).upgrade(tx => {
return tx.table('notes').toCollection().modify(note => {
if (note.deletedAt === undefined) {
note.deletedAt = null
}
})
})
}
}
export const db = new NotesDB()
// Module-level singleton prevents double-seeding under React StrictMode
let _seedPromise: Promise<void> | null = null
export function seedIfEmpty(): Promise<void> {
if (!_seedPromise) _seedPromise = _doSeed()
return _seedPromise
}
async function _doSeed() {
// 用文件夹数量做判断,防止 notes 为空但文件夹已存在时重复创建
const [noteCount, folderCount] = await Promise.all([
db.notes.count(),
db.folders.count(),
])
if (noteCount > 0 || folderCount > 0) return
const folderId1 = crypto.randomUUID()
const folderId2 = crypto.randomUUID()
const now = Date.now()
await db.folders.bulkAdd([
{ id: folderId1, name: '工作', parentId: null, order: 0, createdAt: now },
{ id: folderId2, name: '个人', parentId: null, order: 1, createdAt: now },
])
await db.notes.bulkAdd([
{
id: crypto.randomUUID(),
title: '项目计划',
content: JSON.stringify({
type: 'doc',
content: [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: '项目计划' }] },
{ type: 'taskList', content: [
{ type: 'taskItem', attrs: { checked: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '需求分析' }] }] },
{ type: 'taskItem', attrs: { checked: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '技术选型' }] }] },
{ type: 'taskItem', attrs: { checked: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '开发实现' }] }] },
{ type: 'taskItem', attrs: { checked: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '测试上线' }] }] },
]},
],
}),
folderId: folderId1,
tags: ['工作', '计划'],
starred: false,
createdAt: now - 86400000,
updatedAt: now - 3600000,
wordCount: 30,
deletedAt: null,
},
{
id: crypto.randomUUID(),
title: '读书笔记',
content: JSON.stringify({
type: 'doc',
content: [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: '读书笔记' }] },
{ type: 'paragraph', content: [{ type: 'text', text: '今天读了《深度工作》,记录几点感想:' }] },
{ type: 'blockquote', content: [{ type: 'paragraph', content: [{ type: 'text', text: '深度工作是指在无干扰的专注状态下完成认知要求高的工作。' }] }] },
],
}),
folderId: folderId2,
tags: ['阅读'],
starred: false,
createdAt: now - 172800000,
updatedAt: now - 172800000,
wordCount: 45,
deletedAt: null,
},
])
}
// Removes duplicate folders and notes inserted by React StrictMode double-mount.
// Groups by (name + parentId) for folders, (title + createdAt bucket) for notes.
export async function deduplicateDB() {
// --- folders ---
const allFolders = await db.folders.toArray()
const folderSeen = new Map<string, string>() // key → keep id
const folderRemap = new Map<string, string>() // deleted id → kept id
for (const f of allFolders) {
const key = `${f.parentId ?? ''}::${f.name}`
if (!folderSeen.has(key)) {
folderSeen.set(key, f.id)
} else {
folderRemap.set(f.id, folderSeen.get(key)!)
await db.folders.delete(f.id)
}
}
// Re-point notes that referenced a deleted folder
if (folderRemap.size > 0) {
const notesToFix = await db.notes.filter(n => n.folderId !== null && folderRemap.has(n.folderId!)).toArray()
for (const n of notesToFix) {
await db.notes.update(n.id, { folderId: folderRemap.get(n.folderId!)! })
}
}
// --- notes: deduplicate by title + createdAt within 2 s window ---
const allNotes = await db.notes.toArray()
const noteSeen = new Map<string, number>() // key → kept createdAt
for (const n of allNotes) {
const bucket = Math.floor(n.createdAt / 2000)
const key = `${n.title}::${bucket}`
if (!noteSeen.has(key)) {
noteSeen.set(key, n.createdAt)
} else {
await db.notes.delete(n.id)
}
}
}