refactor: v2.0 全量重构 — TypeScript + React + Zustand + IndexedDB
技术栈升级: - JavaScript → TypeScript 5.6(全量类型安全) - 原生 DOM → React 18(函数组件 + Hooks) - 全局变量 → Zustand 5(轻量状态管理) - localStorage → IndexedDB / Dexie.js 4(大容量、异步、索引) - marked.js → unified / remark / rehype(插件化渲染管线) - 无打包 → electron-vite 3(HMR 热更新) - 纯 CSS → CSS Variables + CSS Modules 新增功能: - 标签页状态 IndexedDB 持久化(关闭后可恢复) - 最近打开文件列表 - 大文件虚拟化行号(>2000 行) - 搜索高亮二分查找优化 O(log N) - rehype-sanitize HTML 安全过滤 文件结构: - 7 个源文件 → 51 个模块化文件 - src/main/ 主进程(6 文件) - src/preload/ 预加载(1 文件) - src/renderer/ 渲染进程(42 文件:组件/hooks/stores/lib/db/types/styles) - src/shared/ 共享类型(2 文件) 构建验证: - TypeScript 检查零错误 - electron-vite build 成功 - 产物:main 15kB + preload 2kB + renderer 1.5MB
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { create } from 'zustand'
|
||||
import type { ViewMode } from '../types/settings'
|
||||
|
||||
interface EditorState {
|
||||
viewMode: ViewMode
|
||||
darkMode: boolean
|
||||
splitRatio: number
|
||||
|
||||
setViewMode: (mode: ViewMode) => void
|
||||
setDarkMode: (dark: boolean) => void
|
||||
setSplitRatio: (ratio: number) => void
|
||||
toggleDarkMode: () => void
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>((set) => ({
|
||||
viewMode: 'split',
|
||||
darkMode: false,
|
||||
splitRatio: 50,
|
||||
|
||||
setViewMode: (mode) => set({ viewMode: mode }),
|
||||
setDarkMode: (dark) => set({ darkMode: dark }),
|
||||
setSplitRatio: (ratio) => set({ splitRatio: ratio }),
|
||||
toggleDarkMode: () => set(state => ({ darkMode: !state.darkMode }))
|
||||
}))
|
||||
@@ -0,0 +1,64 @@
|
||||
import { create } from 'zustand'
|
||||
import type { SearchMatch, SearchOptions } from '../types/search'
|
||||
|
||||
interface SearchState {
|
||||
isVisible: boolean
|
||||
showReplace: boolean
|
||||
searchText: string
|
||||
replaceText: string
|
||||
matches: SearchMatch[]
|
||||
currentIndex: number
|
||||
options: SearchOptions
|
||||
|
||||
setVisible: (visible: boolean) => void
|
||||
setShowReplace: (show: boolean) => void
|
||||
setSearchText: (text: string) => void
|
||||
setReplaceText: (text: string) => void
|
||||
setMatches: (matches: SearchMatch[]) => void
|
||||
setCurrentIndex: (index: number) => void
|
||||
toggleCaseSensitive: () => void
|
||||
toggleRegex: () => void
|
||||
findNext: () => void
|
||||
findPrev: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const useSearchStore = create<SearchState>((set, get) => ({
|
||||
isVisible: false,
|
||||
showReplace: false,
|
||||
searchText: '',
|
||||
replaceText: '',
|
||||
matches: [],
|
||||
currentIndex: -1,
|
||||
options: { caseSensitive: false, useRegex: false },
|
||||
|
||||
setVisible: (visible) => set({ isVisible: visible }),
|
||||
setShowReplace: (show) => set({ showReplace: show }),
|
||||
setSearchText: (text) => set({ searchText: text }),
|
||||
setReplaceText: (text) => set({ replaceText: text }),
|
||||
setMatches: (matches) => set({ matches, currentIndex: matches.length > 0 ? 0 : -1 }),
|
||||
setCurrentIndex: (index) => set({ currentIndex: index }),
|
||||
toggleCaseSensitive: () =>
|
||||
set(state => ({ options: { ...state.options, caseSensitive: !state.options.caseSensitive } })),
|
||||
toggleRegex: () =>
|
||||
set(state => ({ options: { ...state.options, useRegex: !state.options.useRegex } })),
|
||||
findNext: () => {
|
||||
const { matches, currentIndex } = get()
|
||||
if (matches.length === 0) return
|
||||
set({ currentIndex: (currentIndex + 1) % matches.length })
|
||||
},
|
||||
findPrev: () => {
|
||||
const { matches, currentIndex } = get()
|
||||
if (matches.length === 0) return
|
||||
set({ currentIndex: (currentIndex - 1 + matches.length) % matches.length })
|
||||
},
|
||||
close: () =>
|
||||
set({
|
||||
isVisible: false,
|
||||
showReplace: false,
|
||||
searchText: '',
|
||||
replaceText: '',
|
||||
matches: [],
|
||||
currentIndex: -1
|
||||
})
|
||||
}))
|
||||
@@ -0,0 +1,39 @@
|
||||
import { create } from 'zustand'
|
||||
import type { FileNode } from '../types/file'
|
||||
|
||||
interface SidebarState {
|
||||
isVisible: boolean
|
||||
rootPath: string | null
|
||||
tree: FileNode[]
|
||||
expandedDirs: Set<string>
|
||||
sidebarWidth: number
|
||||
|
||||
setVisible: (visible: boolean) => void
|
||||
setRootPath: (path: string | null) => void
|
||||
setTree: (tree: FileNode[]) => void
|
||||
toggleDir: (path: string) => void
|
||||
setSidebarWidth: (width: number) => void
|
||||
}
|
||||
|
||||
export const useSidebarStore = create<SidebarState>((set) => ({
|
||||
isVisible: true,
|
||||
rootPath: null,
|
||||
tree: [],
|
||||
expandedDirs: new Set<string>(),
|
||||
sidebarWidth: 240,
|
||||
|
||||
setVisible: (visible) => set({ isVisible: visible }),
|
||||
setRootPath: (path) => set({ rootPath: path }),
|
||||
setTree: (tree) => set({ tree }),
|
||||
toggleDir: (path) =>
|
||||
set(state => {
|
||||
const newSet = new Set(state.expandedDirs)
|
||||
if (newSet.has(path)) {
|
||||
newSet.delete(path)
|
||||
} else {
|
||||
newSet.add(path)
|
||||
}
|
||||
return { expandedDirs: newSet }
|
||||
}),
|
||||
setSidebarWidth: (width) => set({ sidebarWidth: width })
|
||||
}))
|
||||
@@ -0,0 +1,127 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Tab } from '../types/tab'
|
||||
|
||||
interface TabState {
|
||||
tabs: Tab[]
|
||||
activeTabId: string | null
|
||||
mruStack: string[]
|
||||
|
||||
createTab: (filePath?: string | null, content?: string) => Tab
|
||||
closeTab: (tabId: string) => void
|
||||
switchToTab: (tabId: string) => void
|
||||
updateTabContent: (tabId: string, content: string) => void
|
||||
setModified: (tabId: string, modified: boolean) => void
|
||||
getActiveTab: () => Tab | null
|
||||
getTabIndex: (tabId: string) => number
|
||||
updateTabScroll: (tabId: string, scroll: Partial<Pick<Tab, 'scrollTop' | 'scrollLeft' | 'selectionStart' | 'selectionEnd' | 'previewScrollTop'>>) => void
|
||||
}
|
||||
|
||||
let tabIdCounter = 0
|
||||
|
||||
export const useTabStore = create<TabState>((set, get) => ({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
mruStack: [],
|
||||
|
||||
createTab: (filePath = null, content = '') => {
|
||||
// 检查是否已打开
|
||||
if (filePath) {
|
||||
const existing = get().tabs.find(t => t.filePath === filePath)
|
||||
if (existing) {
|
||||
get().switchToTab(existing.id)
|
||||
return existing
|
||||
}
|
||||
}
|
||||
|
||||
const tab: Tab = {
|
||||
id: String(++tabIdCounter),
|
||||
filePath,
|
||||
content,
|
||||
isModified: false,
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
previewScrollTop: 0
|
||||
}
|
||||
|
||||
set(state => ({
|
||||
tabs: [...state.tabs, tab],
|
||||
activeTabId: tab.id
|
||||
}))
|
||||
|
||||
return tab
|
||||
},
|
||||
|
||||
closeTab: (tabId) => {
|
||||
set(state => {
|
||||
const index = state.tabs.findIndex(t => t.id === tabId)
|
||||
if (index === -1) return state
|
||||
|
||||
const newTabs = state.tabs.filter(t => t.id !== tabId)
|
||||
const newMru = state.mruStack.filter(id => id !== tabId)
|
||||
|
||||
let newActiveId = state.activeTabId
|
||||
if (state.activeTabId === tabId) {
|
||||
if (newTabs.length === 0) {
|
||||
newActiveId = null
|
||||
} else {
|
||||
const newIndex = Math.min(index, newTabs.length - 1)
|
||||
newActiveId = newTabs[newIndex].id
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tabs: newTabs,
|
||||
activeTabId: newActiveId,
|
||||
mruStack: newMru
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
switchToTab: (tabId) => {
|
||||
set(state => {
|
||||
if (state.activeTabId === tabId) return state
|
||||
const newMru = state.activeTabId
|
||||
? [state.activeTabId, ...state.mruStack.filter(id => id !== state.activeTabId)]
|
||||
: state.mruStack
|
||||
return {
|
||||
activeTabId: tabId,
|
||||
mruStack: newMru
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
updateTabContent: (tabId, content) => {
|
||||
set(state => ({
|
||||
tabs: state.tabs.map(t =>
|
||||
t.id === tabId ? { ...t, content, isModified: true } : t
|
||||
)
|
||||
}))
|
||||
},
|
||||
|
||||
setModified: (tabId, modified) => {
|
||||
set(state => ({
|
||||
tabs: state.tabs.map(t =>
|
||||
t.id === tabId ? { ...t, isModified: modified } : t
|
||||
)
|
||||
}))
|
||||
},
|
||||
|
||||
getActiveTab: () => {
|
||||
const { tabs, activeTabId } = get()
|
||||
return tabs.find(t => t.id === activeTabId) ?? null
|
||||
},
|
||||
|
||||
getTabIndex: (tabId) => {
|
||||
return get().tabs.findIndex(t => t.id === tabId)
|
||||
},
|
||||
|
||||
updateTabScroll: (tabId, scroll) => {
|
||||
set(state => ({
|
||||
tabs: state.tabs.map(t =>
|
||||
t.id === tabId ? { ...t, ...scroll } : t
|
||||
)
|
||||
}))
|
||||
}
|
||||
}))
|
||||
Reference in New Issue
Block a user