v0.2.0: 全面代码质量优化

- ESLint flat config + Prettier + EditorConfig
- Markdown处理器LRU缓存
- Zustand选择器优化减少重渲染
- CodeMirror Compartment主题热切换
- Preview防抖(150ms) + IndexedDB去抖(500ms)
- 组件拆分: App.tsx 310→104行, Sidebar.tsx 244→90行
- 统一错误处理 errorHandler.ts
- ConfirmDialog替代原生confirm
- LoadingSpinner加载状态
- Toast多条堆叠+类型区分
- 可访问性增强(ARIA属性、键盘导航)
- Vitest测试框架(78个测试用例)
- Git hooks(husky + lint-staged)
- 项目文档(README.md, CONTRIBUTING.md)
This commit is contained in:
thzxx
2026-06-03 22:13:32 +08:00
parent 3cecb0f9eb
commit 7a4e2b0a67
72 changed files with 5103 additions and 894 deletions
+112 -71
View File
@@ -1,7 +1,9 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'
import { useTabStore } from '../../stores/tabStore'
import { useConfirm } from '../../hooks/useConfirm'
import { getFileName } from '../../lib/fileUtils'
import { Close, Plus } from '../Icons'
import { ConfirmDialog } from '../ConfirmDialog/ConfirmDialog'
interface ContextMenuState {
visible: boolean
@@ -22,6 +24,7 @@ export function TabBar() {
const tabListRef = useRef<HTMLDivElement>(null)
const [menu, setMenu] = useState<ContextMenuState>({ visible: false, x: 0, y: 0, tabId: '' })
const { confirm, confirmDialogProps } = useConfirm()
// 滚动到活动标签
const scrollToActiveTab = useCallback(() => {
@@ -70,15 +73,21 @@ export function TabBar() {
return () => tabList.removeEventListener('wheel', handleWheel)
}, [])
const handleClose = useCallback((e: React.MouseEvent, tabId: string) => {
const handleClose = useCallback(async (e: React.MouseEvent, tabId: string) => {
e.stopPropagation()
const tab = tabs.find(t => t.id === tabId)
if (tab?.isModified) {
const name = tab.filePath ? getFileName(tab.filePath) : '未命名'
if (!confirm(`"${name}" 尚未保存,确定要关闭吗?`)) return
const confirmed = await confirm({
title: '关闭标签',
message: `"${name}" 尚未保存,确定要关闭吗?`,
variant: 'warning',
confirmLabel: '关闭'
})
if (!confirmed) return
}
closeTab(tabId)
}, [tabs, closeTab])
}, [tabs, closeTab, confirm])
// C-06: 右键菜单(带边界修正)
const handleContextMenu = useCallback((e: React.MouseEvent, tabId: string) => {
@@ -98,47 +107,71 @@ export function TabBar() {
return () => document.removeEventListener('click', handleClick)
}, [menu.visible])
const handleMenuClose = useCallback(() => {
const handleMenuClose = useCallback(async () => {
const tab = tabs.find(t => t.id === menu.tabId)
if (tab?.isModified) {
const name = tab.filePath ? getFileName(tab.filePath) : '未命名'
if (!confirm(`"${name}" 尚未保存,确定要关闭吗?`)) return
const confirmed = await confirm({
title: '关闭标签',
message: `"${name}" 尚未保存,确定要关闭吗?`,
variant: 'warning',
confirmLabel: '关闭'
})
if (!confirmed) return
}
closeTab(menu.tabId)
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, menu.tabId, closeTab])
}, [tabs, menu.tabId, closeTab, confirm])
const handleMenuCloseOthers = useCallback(() => {
const handleMenuCloseOthers = useCallback(async () => {
const otherModified = tabs.filter(t => t.id !== menu.tabId && t.isModified)
if (otherModified.length > 0) {
const names = otherModified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return
const confirmed = await confirm({
title: '关闭其他标签',
message: `以下文件尚未保存:${names},确定要关闭吗?`,
variant: 'warning',
confirmLabel: '关闭'
})
if (!confirmed) return
}
closeOtherTabs(menu.tabId)
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, menu.tabId, closeOtherTabs])
}, [tabs, menu.tabId, closeOtherTabs, confirm])
const handleMenuCloseAll = useCallback(() => {
const handleMenuCloseAll = useCallback(async () => {
const modified = tabs.filter(t => t.isModified)
if (modified.length > 0) {
const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return
const confirmed = await confirm({
title: '关闭全部标签',
message: `以下文件尚未保存:${names},确定要关闭吗?`,
variant: 'warning',
confirmLabel: '关闭'
})
if (!confirmed) return
}
closeAllTabs()
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, closeAllTabs])
}, [tabs, closeAllTabs, confirm])
const handleMenuCloseRight = useCallback(() => {
const handleMenuCloseRight = useCallback(async () => {
const index = tabs.findIndex(t => t.id === menu.tabId)
const rightTabs = tabs.slice(index + 1)
const modified = rightTabs.filter(t => t.isModified)
if (modified.length > 0) {
const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return
const confirmed = await confirm({
title: '关闭右侧标签',
message: `以下文件尚未保存:${names},确定要关闭吗?`,
variant: 'warning',
confirmLabel: '关闭'
})
if (!confirmed) return
}
closeTabsToRight(menu.tabId)
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, menu.tabId, closeTabsToRight])
}, [tabs, menu.tabId, closeTabsToRight, confirm])
const hasRightTabs = menu.visible && (() => {
const index = tabs.findIndex(t => t.id === menu.tabId)
@@ -148,65 +181,73 @@ export function TabBar() {
if (tabs.length === 0) return null
return (
<div id="tab-bar">
<div id="tab-list" ref={tabListRef} role="tablist" aria-label="标签页">
{tabs.map(tab => (
<div
key={tab.id}
className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`}
role="tab"
aria-selected={tab.id === activeTabId}
onClick={() => switchToTab(tab.id)}
onContextMenu={(e) => handleContextMenu(e, tab.id)}
>
<span className="tab-name">
{tab.filePath ? getFileName(tab.filePath) : '未命名'}
</span>
<button
className="tab-close"
onClick={(e) => handleClose(e, tab.id)}
<>
<div id="tab-bar">
<div id="tab-list" ref={tabListRef} role="tablist" aria-label="标签页">
{tabs.map(tab => (
<div
key={tab.id}
className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`}
role="tab"
aria-selected={tab.id === activeTabId}
tabIndex={tab.id === activeTabId ? 0 : -1}
onClick={() => switchToTab(tab.id)}
onContextMenu={(e) => handleContextMenu(e, tab.id)}
>
<Close size={10} />
</button>
</div>
))}
</div>
<button
className="tab-add-btn"
onClick={() => createTab(null, '')}
title="新建标签页 (Ctrl+T)"
>
<Plus size={14} />
</button>
{/* 右键菜单 */}
{menu.visible && (
<div
className="tab-context-menu"
style={{ left: menu.x, top: menu.y }}
onClick={(e) => e.stopPropagation()}
>
<div className="tab-context-item" onClick={handleMenuClose}>
</div>
{tabs.length > 1 && (
<div className="tab-context-item" onClick={handleMenuCloseOthers}>
<span className="tab-name">
{tab.filePath ? getFileName(tab.filePath) : '未命名'}
</span>
<button
className="tab-close"
onClick={(e) => handleClose(e, tab.id)}
aria-label={`关闭 ${tab.filePath ? getFileName(tab.filePath) : '未命名'}`}
>
<Close size={10} />
</button>
</div>
)}
{hasRightTabs && (
<div className="tab-context-item" onClick={handleMenuCloseRight}>
</div>
)}
<div className="tab-context-divider" />
<div className="tab-context-item" onClick={handleMenuCloseAll}>
</div>
))}
</div>
)}
</div>
<button
className="tab-add-btn"
onClick={() => createTab(null, '')}
title="新建标签页 (Ctrl+T)"
aria-label="新建标签页"
>
<Plus size={14} />
</button>
{/* 右键菜单 */}
{menu.visible && (
<div
className="tab-context-menu"
style={{ left: menu.x, top: menu.y }}
role="menu"
aria-label="标签操作"
onClick={(e) => e.stopPropagation()}
>
<div className="tab-context-item" role="menuitem" onClick={handleMenuClose}>
</div>
{tabs.length > 1 && (
<div className="tab-context-item" role="menuitem" onClick={handleMenuCloseOthers}>
</div>
)}
{hasRightTabs && (
<div className="tab-context-item" role="menuitem" onClick={handleMenuCloseRight}>
</div>
)}
<div className="tab-context-divider" role="separator" />
<div className="tab-context-item" role="menuitem" onClick={handleMenuCloseAll}>
</div>
</div>
)}
</div>
<ConfirmDialog {...confirmDialogProps} />
</>
)
}
TabBar.displayName = 'TabBar'
TabBar.displayName = 'TabBar'