From db94ecfad0092aa9ee463534c4638bcad978584a Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 27 May 2026 21:44:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=96=B0=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E7=8E=B0=E4=BB=A3=E5=8C=96=E5=9B=BE=E6=A0=87=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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 --- .../components/DropOverlay/DropOverlay.tsx | 6 +- src/renderer/components/Icons.tsx | 251 ++++++++++++++++++ .../components/SearchBar/SearchBar.tsx | 18 +- src/renderer/components/Sidebar/Sidebar.tsx | 37 +-- src/renderer/components/TabBar/TabBar.tsx | 17 +- src/renderer/components/Toolbar/Toolbar.tsx | 44 +-- .../WelcomeScreen/WelcomeScreen.tsx | 18 +- 7 files changed, 283 insertions(+), 108 deletions(-) create mode 100644 src/renderer/components/Icons.tsx diff --git a/src/renderer/components/DropOverlay/DropOverlay.tsx b/src/renderer/components/DropOverlay/DropOverlay.tsx index 231f263..bf28141 100644 --- a/src/renderer/components/DropOverlay/DropOverlay.tsx +++ b/src/renderer/components/DropOverlay/DropOverlay.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' +import { UploadCloud } from '../Icons' export function DropOverlay() { - // L-11: 简化为单一状态 const [dragCounter, setDragCounter] = useState(0) useEffect(() => { @@ -41,9 +41,7 @@ export function DropOverlay() { return (
- - - +

释放文件以打开

diff --git a/src/renderer/components/Icons.tsx b/src/renderer/components/Icons.tsx new file mode 100644 index 0000000..800697a --- /dev/null +++ b/src/renderer/components/Icons.tsx @@ -0,0 +1,251 @@ +import React from 'react' + +// 统一图标 Props +interface IconProps { + size?: number + className?: string + style?: React.CSSProperties +} + +const defaultProps: Partial = { size: 18 } + +// ===== 应用图标 ===== +export function AppIcon({ size = 80 }: IconProps) { + return ( + + {/* 背景圆角矩形 */} + + {/* Markdown M↓ 符号 */} + + {/* 底部装饰线 */} + + + + + + + + + + + + + ) +} + +// ===== 工具栏图标 ===== +export function FolderOpen({ size = defaultProps.size }: IconProps) { + return ( + + + + + + ) +} + +export function Save({ size = defaultProps.size }: IconProps) { + return ( + + + + + + ) +} + +export function SplitView({ size = defaultProps.size }: IconProps) { + return ( + + + + + + + + + ) +} + +export function EditMode({ size = defaultProps.size }: IconProps) { + return ( + + + + + + ) +} + +export function PreviewMode({ size = defaultProps.size }: IconProps) { + return ( + + + + + + ) +} + +export function Moon({ size = defaultProps.size }: IconProps) { + return ( + + + + + ) +} + +export function Sun({ size = defaultProps.size }: IconProps) { + return ( + + + + + + + + + + + + + ) +} + +// ===== 标签栏图标 ===== +export function Close({ size = 10 }: IconProps) { + return ( + + + + + ) +} + +export function Plus({ size = 14 }: IconProps) { + return ( + + + + + ) +} + +// ===== 侧边栏图标 ===== +export function Folder({ size = 14 }: IconProps) { + return ( + + + + + ) +} + +export function File({ size = 14 }: IconProps) { + return ( + + + + + + + ) +} + +export function ChevronRight({ size = 10 }: IconProps) { + return ( + + + + ) +} + +export function FolderPlus({ size = 14 }: IconProps) { + return ( + + + + + + ) +} + +// ===== 搜索栏图标 ===== +export function ChevronUp({ size = 12 }: IconProps) { + return ( + + + + ) +} + +export function ChevronDown({ size = 12 }: IconProps) { + return ( + + + + ) +} + +export function X({ size = 14 }: IconProps) { + return ( + + + + + ) +} + +// ===== 拖拽覆盖层图标 ===== +export function UploadCloud({ size = 64 }: IconProps) { + return ( + + + + + + + ) +} + +// ===== 箭头/导航图标 ===== +export function ArrowUp({ size = 12 }: IconProps) { + return ( + + + + + ) +} + +export function ArrowDown({ size = 12 }: IconProps) { + return ( + + + + + ) +} + +// ===== 欢迎屏幕图标 ===== +export function WelcomeFile({ size = 20 }: IconProps) { + return ( + + + + + + ) +} + +export function WelcomeNew({ size = 20 }: IconProps) { + return ( + + + + + + + ) +} diff --git a/src/renderer/components/SearchBar/SearchBar.tsx b/src/renderer/components/SearchBar/SearchBar.tsx index df78e9a..09c5a72 100644 --- a/src/renderer/components/SearchBar/SearchBar.tsx +++ b/src/renderer/components/SearchBar/SearchBar.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useRef, useEffect } from 'react' import { useSearchStore } from '../../stores/searchStore' import { useTabStore } from '../../stores/tabStore' import { findMatches } from '../../lib/searchEngine' +import { ChevronUp, ChevronDown, X } from '../Icons' export function SearchBar() { const searchInputRef = useRef(null) @@ -25,7 +26,6 @@ export function SearchBar() { const getActiveTab = useTabStore(s => s.getActiveTab) - // 执行搜索 const doSearch = useCallback((text: string) => { const tab = getActiveTab() if (!tab || !text) { @@ -36,21 +36,18 @@ export function SearchBar() { setMatches(found) }, [getActiveTab, options, setMatches]) - // 搜索文本变化 const handleSearchChange = useCallback((e: React.ChangeEvent) => { const text = e.target.value setSearchText(text) doSearch(text) }, [setSearchText, doSearch]) - // 替换当前 const handleReplaceCurrent = useCallback(() => { const tab = getActiveTab() if (!tab || matches.length === 0 || currentIndex < 0) return const m = matches[currentIndex] const newContent = tab.content.substring(0, m.start) + replaceText + tab.content.substring(m.end) useTabStore.getState().updateTabContent(tab.id, newContent) - // 重新搜索 doSearch(searchText) }, [getActiveTab, matches, currentIndex, replaceText, searchText, doSearch]) @@ -67,7 +64,6 @@ export function SearchBar() { doSearch(searchText) }, [getActiveTab, matches, replaceText, searchText, doSearch]) - // 打开时聚焦 useEffect(() => { if (isVisible && searchInputRef.current) { searchInputRef.current.focus() @@ -123,16 +119,14 @@ export function SearchBar() { .* + - {showReplace && ( diff --git a/src/renderer/components/Sidebar/Sidebar.tsx b/src/renderer/components/Sidebar/Sidebar.tsx index 57e3b3d..d485436 100644 --- a/src/renderer/components/Sidebar/Sidebar.tsx +++ b/src/renderer/components/Sidebar/Sidebar.tsx @@ -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(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() { @@ -147,10 +139,7 @@ export function Sidebar() { onClick={() => switchToTab(tab.id)} > - - - - + {getFileName(tab.filePath!)} {tab.isModified && } @@ -176,7 +165,6 @@ export function Sidebar() { )} - {/* 调节手柄 */}
setIsResizing(true)} @@ -213,24 +201,17 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi {node.type === 'dir' ? ( <> - - - + - - - + ) : ( <> - - - - + )} diff --git a/src/renderer/components/TabBar/TabBar.tsx b/src/renderer/components/TabBar/TabBar.tsx index 4bf2375..a7074b3 100644 --- a/src/renderer/components/TabBar/TabBar.tsx +++ b/src/renderer/components/TabBar/TabBar.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState, useEffect, useRef } from 'react' import { useTabStore } from '../../stores/tabStore' import { getFileName } from '../../lib/fileUtils' +import { Close, Plus } from '../Icons' interface ContextMenuState { visible: boolean @@ -22,7 +23,6 @@ export function TabBar() { const [menu, setMenu] = useState({ visible: false, x: 0, y: 0, tabId: '' }) const menuRef = useRef(null) - // 关闭标签(带未保存确认) const handleClose = useCallback((e: React.MouseEvent, tabId: string) => { e.stopPropagation() const tab = tabs.find(t => t.id === tabId) @@ -44,7 +44,6 @@ export function TabBar() { setMenu({ visible: true, x: Math.max(0, x), y: Math.max(0, y), tabId }) }, []) - // 点击空白处关闭菜单 useEffect(() => { if (!menu.visible) return const handleClick = () => setMenu(prev => ({ ...prev, visible: false })) @@ -52,7 +51,6 @@ export function TabBar() { return () => document.removeEventListener('click', handleClick) }, [menu.visible]) - // 菜单操作 const handleMenuClose = useCallback(() => { const tab = tabs.find(t => t.id === menu.tabId) if (tab?.isModified) { @@ -64,7 +62,6 @@ export function TabBar() { }, [tabs, menu.tabId, closeTab]) const handleMenuCloseOthers = useCallback(() => { - // 检查其他标签是否有未保存 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('、') @@ -96,7 +93,6 @@ export function TabBar() { setMenu(prev => ({ ...prev, visible: false })) }, [tabs, menu.tabId, closeTabsToRight]) - // 判断右侧是否还有标签 const hasRightTabs = menu.visible && (() => { const index = tabs.findIndex(t => t.id === menu.tabId) return index < tabs.length - 1 @@ -121,10 +117,7 @@ export function TabBar() { className="tab-close" onClick={(e) => handleClose(e, tab.id)} > - - - - +
))} @@ -134,10 +127,7 @@ export function TabBar() { onClick={() => createTab(null, '')} title="新建标签页 (Ctrl+T)" > - - - - + {/* 右键菜单 */} @@ -151,7 +141,6 @@ export function TabBar() {
关闭
- {/* M-03: 单标签时隐藏"关闭其他标签" */} {tabs.length > 1 && (
关闭其他标签 diff --git a/src/renderer/components/Toolbar/Toolbar.tsx b/src/renderer/components/Toolbar/Toolbar.tsx index b0576ac..b204b71 100644 --- a/src/renderer/components/Toolbar/Toolbar.tsx +++ b/src/renderer/components/Toolbar/Toolbar.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { FolderOpen, Save, SplitView, EditMode, PreviewMode, Moon, Sun } from '../Icons' interface ToolbarProps { onOpen: () => void @@ -14,61 +15,30 @@ export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode,
diff --git a/src/renderer/components/WelcomeScreen/WelcomeScreen.tsx b/src/renderer/components/WelcomeScreen/WelcomeScreen.tsx index c252446..ccb8b8b 100644 --- a/src/renderer/components/WelcomeScreen/WelcomeScreen.tsx +++ b/src/renderer/components/WelcomeScreen/WelcomeScreen.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { AppIcon, WelcomeFile, WelcomeNew } from '../Icons' interface WelcomeScreenProps { onOpen: () => void @@ -10,26 +11,17 @@ export function WelcomeScreen({ onOpen, onNew }: WelcomeScreenProps) {
- - - M↓ - MarkLite - +

欢迎使用 MarkLite

-

一款轻量级的 Markdown 阅读器

+

一款轻量级的 Markdown 编辑器