feat: 重新设计现代化图标系统

新增 Icons.tsx 统一图标组件(25 个图标):
- AppIcon: 渐变蓝色圆角矩形 + M↓ 符号
- 工具栏: FolderOpen, Save, SplitView, EditMode, PreviewMode, Moon, Sun
- 标签栏: Close, Plus
- 侧边栏: Folder, File, ChevronRight, FolderPlus
- 搜索栏: ChevronUp, ChevronDown, X
- 拖拽: UploadCloud
- 导航: ArrowUp, ArrowDown
- 欢迎: WelcomeFile, WelcomeNew

设计风格:
- 线性图标(stroke-based),统一 1.75px 线宽
- 圆角风格,现代科技感
- 蓝色主色调渐变应用图标
- 部分图标添加微妙的填充细节(opacity 0.3-0.4)

更新组件:
- Toolbar: 使用 FolderOpen/Save/SplitView/EditMode/PreviewMode/Moon/Sun
- TabBar: 使用 Close/Plus
- Sidebar: 使用 FolderPlus/File/Folder/ChevronRight
- SearchBar: 使用 ChevronUp/ChevronDown/X
- WelcomeScreen: 使用 AppIcon/WelcomeFile/WelcomeNew
- DropOverlay: 使用 UploadCloud
This commit is contained in:
thzxx
2026-05-27 21:44:26 +08:00
parent 70dff0cf01
commit db94ecfad0
7 changed files with 283 additions and 108 deletions
+9 -28
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useCallback, useState, useRef } from 'react'
import { useTabStore } from '../../stores/tabStore'
import { useSidebarStore } from '../../stores/sidebarStore'
import { getFileName } from '../../lib/fileUtils'
import { FolderPlus, File, Folder, ChevronRight } from '../Icons'
import type { FileNode } from '../../types/file'
export function Sidebar() {
@@ -19,22 +20,19 @@ export function Sidebar() {
const isVisible = useSidebarStore(s => s.isVisible)
const expandDirs = useSidebarStore(s => s.expandDirs)
// M-01: 归一化路径分隔符Windows 混合 \\ 和 /
// M-01: 归一化路径分隔符
const norm = (p: string) => p.replace(/\\/g, '/')
const activeFilePath = tabs.find(t => t.id === activeTabId)?.filePath ?? null
// 自动展开到活动文件所在的目录
useEffect(() => {
if (!activeFilePath || !rootPath) return
// M-01: 归一化后比较
const normActive = norm(activeFilePath)
const normRoot = norm(rootPath)
if (!normActive.startsWith(normRoot)) return
// 提取文件的所有父目录路径
const dirsToExpand: string[] = []
let dir = activeFilePath.replace(/[/\\][^/\\]+$/, '') // 移除文件名
// M-02: 添加 prev 防护无限循环
let dir = activeFilePath.replace(/[/\\][^/\\]+$/, '')
let prev = ''
while (dir && dir.length >= rootPath.length && dir !== rootPath && dir !== prev) {
prev = dir
@@ -50,7 +48,6 @@ export function Sidebar() {
const [isResizing, setIsResizing] = useState(false)
const sidebarRef = useRef<HTMLDivElement>(null)
// 打开文件夹
const handleOpenFolder = useCallback(async () => {
if (!window.electronAPI) return
const dirPath = await window.electronAPI.openFolderDialog()
@@ -64,7 +61,6 @@ export function Sidebar() {
}
}, [setRootPath, setTree])
// 刷新目录树
const refreshTree = useCallback(async () => {
if (!rootPath || !window.electronAPI) return
const result = await window.electronAPI.readDirTree(rootPath)
@@ -73,7 +69,6 @@ export function Sidebar() {
}
}, [rootPath, setTree])
// 目录变化监听
useEffect(() => {
if (!window.electronAPI) return
window.electronAPI.onDirChanged(() => {
@@ -84,7 +79,6 @@ export function Sidebar() {
}
}, [refreshTree])
// 侧边栏宽度调节
useEffect(() => {
if (!isResizing) return
const handleMouseMove = (e: MouseEvent) => {
@@ -102,7 +96,6 @@ export function Sidebar() {
}
}, [isResizing])
// L-12: 提取为 useCallback 避免每次渲染重建
const handleFileClick = useCallback(async (path: string) => {
const existing = tabs.find(t => t.filePath === path)
if (existing) {
@@ -114,6 +107,7 @@ export function Sidebar() {
}
}
}, [tabs, switchToTab, createTab])
// M-01: 独立文件区(归一化路径比较)
const independentFiles = tabs.filter(t => {
if (!t.filePath) return false
@@ -128,9 +122,7 @@ export function Sidebar() {
<div id="sidebar-header">
<span id="sidebar-title"></span>
<button className="sidebar-header-btn" onClick={handleOpenFolder} title="打开文件夹">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
<FolderPlus size={14} />
</button>
</div>
@@ -147,10 +139,7 @@ export function Sidebar() {
onClick={() => switchToTab(tab.id)}
>
<span className="tree-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<File size={14} />
</span>
<span className="tree-name">{getFileName(tab.filePath!)}</span>
{tab.isModified && <span className="independent-modified-dot"> </span>}
@@ -176,7 +165,6 @@ export function Sidebar() {
)}
</div>
{/* 调节手柄 */}
<div
className="sidebar-resize-handle"
onMouseDown={() => setIsResizing(true)}
@@ -213,24 +201,17 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
{node.type === 'dir' ? (
<>
<span className={`tree-arrow ${expandedDirs.has(node.path) ? 'expanded' : ''}`}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="9 18 15 12 9 6" />
</svg>
<ChevronRight size={10} />
</span>
<span className="tree-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
<Folder size={14} />
</span>
</>
) : (
<>
<span style={{ width: '16px', flexShrink: 0 }} />
<span className="tree-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<File size={14} />
</span>
</>
)}