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:
@@ -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 }
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user