feat: 标签页 IndexedDB 持久化 + 最近打开文件

1. 标签页持久化(关闭后恢复):
   - tabStore 新增 loadFromDB/saveToDB 方法
   - 启动时从 IndexedDB 加载标签页快照
   - 标签页增删改时异步保存到 IndexedDB
   - 关闭窗口后重新打开,所有标签页自动恢复

2. 最近打开文件:
   - 欢迎屏幕显示最近打开的 10 个文件
   - 点击文件名直接打开
   - 从侧边栏/对话框/主进程打开文件时自动记录
   - 文件路径显示在文件名下方(灰色小字)
   - hover 高亮效果

REFACTOR-PLAN.md 实现进度更新:
   CodeMirror 6 编辑器
   格式化工具栏
   标签页 IndexedDB 持久化
   最近打开文件 UI
   Zustand stores
   TypeScript 全量类型
   unified/rehype
   electron-vite
   CSS Modules(全局 CSS 已足够)
This commit is contained in:
thzxx
2026-05-27 23:21:31 +08:00
parent 1cc4206b32
commit 23304dca1d
5 changed files with 201 additions and 14 deletions
+67 -12
View File
@@ -1,11 +1,13 @@
import { create } from 'zustand'
import { nanoid } from 'nanoid'
import type { Tab } from '../types/tab'
import { tabRepository } from '../db/tabRepository'
interface TabState {
tabs: Tab[]
activeTabId: string | null
mruStack: string[]
_loaded: boolean
createTab: (filePath?: string | null, content?: string) => Tab
closeTab: (tabId: string) => void
@@ -18,16 +20,69 @@ interface TabState {
getActiveTab: () => Tab | null
getTabIndex: (tabId: string) => number
updateTabScroll: (tabId: string, scroll: Partial<Pick<Tab, 'scrollTop' | 'scrollLeft' | 'selectionStart' | 'selectionEnd' | 'previewScrollTop'>>) => void
loadFromDB: () => Promise<void>
saveToDB: () => Promise<void>
}
// L-04: 使用 nanoid 替代计数器(HMR 安全)
export const useTabStore = create<TabState>((set, get) => ({
tabs: [],
activeTabId: null,
mruStack: [],
_loaded: false,
// 从 IndexedDB 加载标签页快照
loadFromDB: async () => {
if (get()._loaded) return
try {
const snapshots = await tabRepository.loadAll()
if (snapshots.length > 0) {
const tabs: Tab[] = snapshots.map(s => ({
id: s.id,
filePath: s.filePath,
content: s.content,
isModified: s.isModified,
scrollTop: s.scrollTop,
scrollLeft: s.scrollLeft,
selectionStart: s.selectionStart,
selectionEnd: s.selectionEnd,
previewScrollTop: s.previewScrollTop
}))
set({
tabs,
activeTabId: tabs[tabs.length - 1].id,
_loaded: true
})
} else {
set({ _loaded: true })
}
} catch {
set({ _loaded: true })
}
},
// 保存当前标签页到 IndexedDB
saveToDB: async () => {
const { tabs } = get()
if (tabs.length === 0) {
await tabRepository.clearAll()
return
}
const snapshots = tabs.map(t => ({
id: t.id,
filePath: t.filePath,
content: t.content,
isModified: t.isModified,
scrollTop: t.scrollTop,
scrollLeft: t.scrollLeft,
selectionStart: t.selectionStart,
selectionEnd: t.selectionEnd,
previewScrollTop: t.previewScrollTop,
updatedAt: Date.now()
}))
await tabRepository.saveAll(snapshots)
},
createTab: (filePath = null, content = '') => {
// 检查是否已打开
if (filePath) {
const existing = get().tabs.find(t => t.filePath === filePath)
if (existing) {
@@ -53,6 +108,9 @@ export const useTabStore = create<TabState>((set, get) => ({
activeTabId: tab.id
}))
// 异步保存
setTimeout(() => get().saveToDB(), 0)
return tab
},
@@ -69,7 +127,6 @@ export const useTabStore = create<TabState>((set, get) => ({
if (newTabs.length === 0) {
newActiveId = null
} else {
// M-05: 优先从 MRU 栈取最近使用的标签
const mruCandidate = newMru.find(id => newTabs.some(t => t.id === id))
if (mruCandidate) {
newActiveId = mruCandidate
@@ -87,22 +144,22 @@ export const useTabStore = create<TabState>((set, get) => ({
mruStack: newMru
}
})
setTimeout(() => get().saveToDB(), 0)
},
closeOtherTabs: (tabId) => {
set(state => {
const target = state.tabs.find(t => t.id === tabId)
if (!target) return state
return {
tabs: [target],
activeTabId: tabId,
mruStack: []
}
return { tabs: [target], activeTabId: tabId, mruStack: [] }
})
setTimeout(() => get().saveToDB(), 0)
},
closeAllTabs: () => {
set({ tabs: [], activeTabId: null, mruStack: [] })
setTimeout(() => get().saveToDB(), 0)
},
closeTabsToRight: (tabId) => {
@@ -119,6 +176,7 @@ export const useTabStore = create<TabState>((set, get) => ({
mruStack: state.mruStack.filter(id => newTabs.some(t => t.id === id))
}
})
setTimeout(() => get().saveToDB(), 0)
},
switchToTab: (tabId) => {
@@ -127,10 +185,7 @@ export const useTabStore = create<TabState>((set, get) => ({
const newMru = state.activeTabId
? [state.activeTabId, ...state.mruStack.filter(id => id !== state.activeTabId)]
: state.mruStack
return {
activeTabId: tabId,
mruStack: newMru
}
return { activeTabId: tabId, mruStack: newMru }
})
},