v0.1.1: 多Agent代码质量审查+优化

- fix: useCodeMirror onChange闭包过期导致编辑器内容丢失同步(P0)
- feat: 全部组件添加displayName(React DevTools调试支持)
- feat: 全部组件添加ARIA属性(WCAG无障碍支持)
- fix: App异步操作添加try-catch错误边界
- fix: tabStore空catch块添加错误日志
- fix: sidebarStore Set→string[](Zustand可序列化)
- fix: useDragDrop readFile错误处理+空文件修复
- fix: useSettings/useTheme load()添加错误处理
- fix: useTheme toggleDarkMode引用优化
- chore: 版本号升级至0.1.1
This commit is contained in:
thzxx
2026-06-03 21:02:56 +08:00
parent d3c4062c10
commit 3cecb0f9eb
16 changed files with 106 additions and 66 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# MarkLite v0.1.0 — 架构设计文档 # MarkLite v0.1.1 — 架构设计文档
## 1. 项目概述 ## 1. 项目概述
+1 -1
View File
@@ -14,7 +14,7 @@
<img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript"> <img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript">
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React"> <img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
<img src="https://img.shields.io/badge/Version-v0.1.0-orange?style=flat-square" alt="Version"> <img src="https://img.shields.io/badge/Version-v0.1.1-orange?style=flat-square" alt="Version">
</p> </p>
<p align="center"> <p align="center">
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "marklite", "name": "marklite",
"version": "0.1.0", "version": "0.1.1",
"description": "Lightweight Markdown Editor for Windows", "description": "Lightweight Markdown Editor for Windows",
"main": "./dist/main/index.js", "main": "./dist/main/index.js",
"scripts": { "scripts": {
+20 -4
View File
@@ -21,9 +21,9 @@ import { ModifiedBanner } from './components/ModifiedBanner/ModifiedBanner'
import { DropOverlay } from './components/DropOverlay/DropOverlay' import { DropOverlay } from './components/DropOverlay/DropOverlay'
import { AppIcon, Gitee } from './components/Icons' import { AppIcon, Gitee } from './components/Icons'
const APP_VERSION = 'v0.1.0' const APP_VERSION = 'v0.1.1'
export default function App() { export function App() {
const tabs = useTabStore(s => s.tabs) const tabs = useTabStore(s => s.tabs)
const activeTabId = useTabStore(s => s.activeTabId) const activeTabId = useTabStore(s => s.activeTabId)
const getActiveTab = useTabStore(s => s.getActiveTab) const getActiveTab = useTabStore(s => s.getActiveTab)
@@ -89,6 +89,7 @@ export default function App() {
// 打开文件 // 打开文件
const handleOpenFile = useCallback(async () => { const handleOpenFile = useCallback(async () => {
try {
if (window.electronAPI) { if (window.electronAPI) {
const result = await window.electronAPI.openFile() const result = await window.electronAPI.openFile()
if (result && 'filePath' in result) { if (result && 'filePath' in result) {
@@ -97,10 +98,14 @@ export default function App() {
setTimeout(() => useTabStore.getState().saveToDB(), 100) setTimeout(() => useTabStore.getState().saveToDB(), 100)
} }
} }
}, [createTab]) } catch {
showToast('操作失败')
}
}, [createTab, showToast])
// 保存文件 // 保存文件
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
try {
const tab = getActiveTab() const tab = getActiveTab()
if (!tab) return if (!tab) return
if (window.electronAPI) { if (window.electronAPI) {
@@ -113,10 +118,14 @@ export default function App() {
showToast('已保存') showToast('已保存')
} }
} }
} catch {
showToast('操作失败')
}
}, [getActiveTab, showToast]) }, [getActiveTab, showToast])
// 另存为 // 另存为
const handleSaveAs = useCallback(async () => { const handleSaveAs = useCallback(async () => {
try {
const tab = getActiveTab() const tab = getActiveTab()
if (!tab) return if (!tab) return
if (window.electronAPI) { if (window.electronAPI) {
@@ -126,6 +135,9 @@ export default function App() {
showToast('已保存') showToast('已保存')
} }
} }
} catch {
showToast('操作失败')
}
}, [getActiveTab, showToast]) }, [getActiveTab, showToast])
// 快捷键 // 快捷键
@@ -247,7 +259,7 @@ export default function App() {
{/* 关于弹框 */} {/* 关于弹框 */}
{showAbout && ( {showAbout && (
<div className="about-overlay" onClick={() => setShowAbout(false)}> <div className="about-overlay" onClick={() => setShowAbout(false)} role="dialog" aria-modal="true" aria-label="关于 MarkLite">
<div className="about-dialog" onClick={e => e.stopPropagation()}> <div className="about-dialog" onClick={e => e.stopPropagation()}>
<div className="about-header"> <div className="about-header">
<AppIcon size={64} /> <AppIcon size={64} />
@@ -289,3 +301,7 @@ export default function App() {
</div> </div>
) )
} }
App.displayName = 'App'
export default App
+3 -1
View File
@@ -87,9 +87,11 @@ export function Editor({ darkMode }: EditorProps) {
}, [viewRef]) }, [viewRef])
return ( return (
<div className="editor-container"> <div className="editor-container" role="region" aria-label="Markdown编辑器">
<EditorToolbar viewRef={viewRef} /> <EditorToolbar viewRef={viewRef} />
<div ref={containerRef} className="codemirror-wrapper" /> <div ref={containerRef} className="codemirror-wrapper" />
</div> </div>
) )
} }
Editor.displayName = 'Editor'
@@ -17,6 +17,12 @@ export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOpti
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null) const viewRef = useRef<EditorView | null>(null)
const isExternalUpdate = useRef(false) const isExternalUpdate = useRef(false)
const onChangeRef = useRef(onChange)
// 始终保持 onChangeRef 为最新回调
useEffect(() => {
onChangeRef.current = onChange
}, [onChange])
// 初始化编辑器 // 初始化编辑器
useEffect(() => { useEffect(() => {
@@ -61,7 +67,7 @@ export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOpti
EditorView.lineWrapping, EditorView.lineWrapping,
EditorView.updateListener.of(update => { EditorView.updateListener.of(update => {
if (update.docChanged && !isExternalUpdate.current) { if (update.docChanged && !isExternalUpdate.current) {
onChange(update.state.doc.toString()) onChangeRef.current(update.state.doc.toString())
} }
}) })
] ]
@@ -56,8 +56,13 @@ export function Preview() {
ref={previewRef} ref={previewRef}
id="preview" id="preview"
className="markdown-body" className="markdown-body"
role="region"
aria-label="Markdown预览"
aria-live="polite"
onClick={handleClick} onClick={handleClick}
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
) )
} }
Preview.displayName = 'Preview'
+9 -4
View File
@@ -128,7 +128,7 @@ export function Sidebar() {
</button> </button>
</div> </div>
<div id="sidebar-tree"> <div id="sidebar-tree" role="tree" aria-label="文件树">
{/* 独立文件区 */} {/* 独立文件区 */}
{independentFiles.length > 0 && ( {independentFiles.length > 0 && (
<div className="independent-files-section"> <div className="independent-files-section">
@@ -138,6 +138,7 @@ export function Sidebar() {
key={tab.id} key={tab.id}
className={`tree-item independent-file-item ${tab.id === activeTabId ? 'active' : ''}`} className={`tree-item independent-file-item ${tab.id === activeTabId ? 'active' : ''}`}
style={{ paddingLeft: '8px' }} style={{ paddingLeft: '8px' }}
role="treeitem"
onClick={() => switchToTab(tab.id)} onClick={() => switchToTab(tab.id)}
> >
<span className="tree-icon"> <span className="tree-icon">
@@ -179,7 +180,7 @@ export function Sidebar() {
function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFilePath, onFileClick }: { function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFilePath, onFileClick }: {
nodes: FileNode[] nodes: FileNode[]
depth: number depth: number
expandedDirs: Set<string> expandedDirs: string[]
toggleDir: (path: string) => void toggleDir: (path: string) => void
activeTabId: string | null activeTabId: string | null
activeFilePath: string | null activeFilePath: string | null
@@ -192,6 +193,7 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
<div <div
className={`tree-item ${node.type === 'file' && node.path === activeFilePath ? 'active' : ''}`} className={`tree-item ${node.type === 'file' && node.path === activeFilePath ? 'active' : ''}`}
style={{ paddingLeft: (8 + depth * 16) + 'px' }} style={{ paddingLeft: (8 + depth * 16) + 'px' }}
role="treeitem"
onClick={() => { onClick={() => {
if (node.type === 'dir') { if (node.type === 'dir') {
toggleDir(node.path) toggleDir(node.path)
@@ -202,7 +204,7 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
> >
{node.type === 'dir' ? ( {node.type === 'dir' ? (
<> <>
<span className={`tree-arrow ${expandedDirs.has(node.path) ? 'expanded' : ''}`}> <span className={`tree-arrow ${expandedDirs.includes(node.path) ? 'expanded' : ''}`}>
<ChevronRight size={10} /> <ChevronRight size={10} />
</span> </span>
<span className="tree-icon"> <span className="tree-icon">
@@ -219,7 +221,7 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
)} )}
<span className="tree-name">{node.name}</span> <span className="tree-name">{node.name}</span>
</div> </div>
{node.type === 'dir' && expandedDirs.has(node.path) && node.children && ( {node.type === 'dir' && expandedDirs.includes(node.path) && node.children && (
<FileTree <FileTree
nodes={node.children} nodes={node.children}
depth={depth + 1} depth={depth + 1}
@@ -235,3 +237,6 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
</> </>
) )
} }
Sidebar.displayName = 'Sidebar'
FileTree.displayName = 'FileTree'
@@ -9,7 +9,7 @@ export function StatusBar() {
const tab = tabs.find(t => t.id === activeTabId) ?? null const tab = tabs.find(t => t.id === activeTabId) ?? null
return ( return (
<div id="statusbar"> <div id="statusbar" role="status" aria-live="polite">
<div className="status-left"> <div className="status-left">
<span id="status-text"> <span id="status-text">
{tab ? (tab.filePath ? getFileName(tab.filePath) : '未命名') : '就绪'} {tab ? (tab.filePath ? getFileName(tab.filePath) : '未命名') : '就绪'}
@@ -23,3 +23,5 @@ export function StatusBar() {
</div> </div>
) )
} }
StatusBar.displayName = 'StatusBar'
+5 -1
View File
@@ -149,11 +149,13 @@ export function TabBar() {
return ( return (
<div id="tab-bar"> <div id="tab-bar">
<div id="tab-list" ref={tabListRef}> <div id="tab-list" ref={tabListRef} role="tablist" aria-label="标签页">
{tabs.map(tab => ( {tabs.map(tab => (
<div <div
key={tab.id} key={tab.id}
className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`} className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`}
role="tab"
aria-selected={tab.id === activeTabId}
onClick={() => switchToTab(tab.id)} onClick={() => switchToTab(tab.id)}
onContextMenu={(e) => handleContextMenu(e, tab.id)} onContextMenu={(e) => handleContextMenu(e, tab.id)}
> >
@@ -206,3 +208,5 @@ export function TabBar() {
</div> </div>
) )
} }
TabBar.displayName = 'TabBar'
+3 -1
View File
@@ -13,7 +13,7 @@ interface ToolbarProps {
export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode, onToggleDark, onShowAbout }: ToolbarProps) { export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode, onToggleDark, onShowAbout }: ToolbarProps) {
return ( return (
<div id="toolbar"> <div id="toolbar" role="toolbar" aria-label="工具栏">
<div className="toolbar-left"> <div className="toolbar-left">
<button className="toolbar-btn" onClick={onOpen} title="打开文件 (Ctrl+O)"> <button className="toolbar-btn" onClick={onOpen} title="打开文件 (Ctrl+O)">
<FolderOpen size={18} /> <FolderOpen size={18} />
@@ -44,3 +44,5 @@ export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode,
</div> </div>
) )
} }
Toolbar.displayName = 'Toolbar'
+5 -1
View File
@@ -26,10 +26,14 @@ export function useDragDrop(showToast: (msg: string) => void) {
} }
if (window.electronAPI) { if (window.electronAPI) {
try {
const result = await window.electronAPI.readFile(filePath) const result = await window.electronAPI.readFile(filePath)
if (result.success && result.content) { if (result.success && result.content !== undefined) {
createTab(filePath, result.content) createTab(filePath, result.content)
} }
} catch (err) {
console.error('Failed to read file via electronAPI:', err)
}
} else { } else {
const reader = new FileReader() const reader = new FileReader()
reader.onload = (ev) => { reader.onload = (ev) => {
+1 -1
View File
@@ -11,7 +11,7 @@ export function useSettings() {
useEffect(() => { useEffect(() => {
settingsRepository.load().then(settings => { settingsRepository.load().then(settings => {
setViewMode(settings.viewMode ?? 'editor') setViewMode(settings.viewMode ?? 'editor')
}) }).catch(err => console.error('Failed to load settings:', err))
}, [setViewMode]) }, [setViewMode])
// M-10: 使用 useCallback 稳定函数引用 // M-10: 使用 useCallback 稳定函数引用
+4 -2
View File
@@ -13,7 +13,7 @@ export function useTheme() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
setDarkMode(settings.darkMode !== undefined ? settings.darkMode : prefersDark) setDarkMode(settings.darkMode !== undefined ? settings.darkMode : prefersDark)
isInitialized.current = true isInitialized.current = true
}) }).catch(err => console.error('Failed to load theme settings:', err))
}, [setDarkMode]) }, [setDarkMode])
// B-05: 首次加载完成后才保存主题设置 // B-05: 首次加载完成后才保存主题设置
@@ -23,5 +23,7 @@ export function useTheme() {
settingsRepository.save({ darkMode }) settingsRepository.save({ darkMode })
}, [darkMode]) }, [darkMode])
return { darkMode, toggleDarkMode: useEditorStore(s => s.toggleDarkMode) } const toggleDarkMode = useEditorStore(s => s.toggleDarkMode)
return { darkMode, toggleDarkMode }
} }
+11 -20
View File
@@ -6,7 +6,7 @@ interface SidebarState {
isVisible: boolean isVisible: boolean
rootPath: string | null rootPath: string | null
tree: FileNode[] tree: FileNode[]
expandedDirs: Set<string> expandedDirs: string[]
sidebarWidth: number sidebarWidth: number
_loaded: boolean _loaded: boolean
@@ -23,7 +23,7 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
isVisible: true, isVisible: true,
rootPath: null, rootPath: null,
tree: [], tree: [],
expandedDirs: new Set<string>(), expandedDirs: [],
sidebarWidth: 240, sidebarWidth: 240,
_loaded: false, _loaded: false,
@@ -50,26 +50,17 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
setRootPath: (path) => set({ rootPath: path }), setRootPath: (path) => set({ rootPath: path }),
setTree: (tree) => set({ tree }), setTree: (tree) => set({ tree }),
toggleDir: (path) => toggleDir: (path) =>
set(state => { set(state => ({
const newSet = new Set(state.expandedDirs) expandedDirs: state.expandedDirs.includes(path)
if (newSet.has(path)) { ? state.expandedDirs.filter(p => p !== path)
newSet.delete(path) : [...state.expandedDirs, path]
} else { })),
newSet.add(path)
}
return { expandedDirs: newSet }
}),
expandDirs: (paths) => expandDirs: (paths) =>
set(state => { set(state => {
const newSet = new Set(state.expandedDirs) const newDirs = paths.filter(p => !state.expandedDirs.includes(p))
let changed = false return newDirs.length > 0
for (const p of paths) { ? { expandedDirs: [...state.expandedDirs, ...newDirs] }
if (!newSet.has(p)) { : state
newSet.add(p)
changed = true
}
}
return changed ? { expandedDirs: newSet } : state
}), }),
setSidebarWidth: (width) => { setSidebarWidth: (width) => {
set({ sidebarWidth: width }) set({ sidebarWidth: width })
+2 -1
View File
@@ -59,7 +59,8 @@ export const useTabStore = create<TabState>((set, get) => ({
} else { } else {
set({ _loaded: true }) set({ _loaded: true })
} }
} catch { } catch (err) {
console.error('Failed to load tabs from DB:', err)
set({ _loaded: true }) set({ _loaded: true })
} }
}, },