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
@@ -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 (
<div id="drop-overlay">
<div className="drop-content">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" 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>
<UploadCloud size={64} />
<p></p>
</div>
</div>
+251
View File
@@ -0,0 +1,251 @@
import React from 'react'
// 统一图标 Props
interface IconProps {
size?: number
className?: string
style?: React.CSSProperties
}
const defaultProps: Partial<IconProps> = { size: 18 }
// ===== 应用图标 =====
export function AppIcon({ size = 80 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* 背景圆角矩形 */}
<rect x="4" y="4" width="72" height="72" rx="16" fill="url(#app-bg)" stroke="url(#app-stroke)" strokeWidth="2"/>
{/* Markdown M↓ 符号 */}
<path d="M22 28v24M22 52V40l8-8 8 8v12M46 28l6 12 6-12v24" stroke="white" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round"/>
{/* 底部装饰线 */}
<rect x="16" y="60" width="48" height="3" rx="1.5" fill="rgba(255,255,255,0.3)"/>
<defs>
<linearGradient id="app-bg" x1="4" y1="4" x2="76" y2="76" gradientUnits="userSpaceOnUse">
<stop stopColor="#3B82F6"/>
<stop offset="1" stopColor="#1D4ED8"/>
</linearGradient>
<linearGradient id="app-stroke" x1="4" y1="4" x2="76" y2="76" gradientUnits="userSpaceOnUse">
<stop stopColor="#60A5FA"/>
<stop offset="1" stopColor="#2563EB"/>
</linearGradient>
</defs>
</svg>
)
}
// ===== 工具栏图标 =====
export function FolderOpen({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 19a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v1"/>
<path d="M20.5 15H5a2 2 0 0 0-2 2l1.5 7h18l2-7a2 2 0 0 0-2-2h-2.5z" fill="none"/>
<path d="M12 11h4" strokeDasharray="2 2"/>
</svg>
)
}
export function Save({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
)
}
export function SplitView({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3"/>
<line x1="12" y1="3" x2="12" y2="21"/>
<rect x="5" y="7" width="5" height="2" rx="1" fill="currentColor" opacity="0.4"/>
<rect x="5" y="11" width="3" height="2" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="14" y="7" width="5" height="2" rx="1" fill="currentColor" opacity="0.4"/>
<rect x="14" y="11" width="3" height="2" rx="1" fill="currentColor" opacity="0.3"/>
</svg>
)
}
export function EditMode({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
<circle cx="18" cy="6" r="1" fill="currentColor"/>
</svg>
)
}
export function PreviewMode({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
<circle cx="12" cy="12" r="1" fill="currentColor"/>
</svg>
)
}
export function Moon({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
<circle cx="19" cy="5" r="1" fill="currentColor" opacity="0.5"/>
</svg>
)
}
export function Sun({ size = defaultProps.size }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="5"/>
<circle cx="12" cy="12" r="2" fill="currentColor" opacity="0.3"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
)
}
// ===== 标签栏图标 =====
export function Close({ size = 10 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
)
}
export function Plus({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
)
}
// ===== 侧边栏图标 =====
export function Folder({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" 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"/>
<line x1="9" y1="13" x2="15" y2="13" opacity="0.4"/>
</svg>
)
}
export function File({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<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"/>
<line x1="8" y1="13" x2="16" y2="13" opacity="0.4"/>
<line x1="8" y1="17" x2="13" y2="17" opacity="0.3"/>
</svg>
)
}
export function ChevronRight({ size = 10 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
)
}
export function FolderPlus({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" 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"/>
<line x1="12" y1="11" x2="12" y2="17"/>
<line x1="9" y1="14" x2="15" y2="14"/>
</svg>
)
}
// ===== 搜索栏图标 =====
export function ChevronUp({ size = 12 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15"/>
</svg>
)
}
export function ChevronDown({ size = 12 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
)
}
export function X({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
)
}
// ===== 拖拽覆盖层图标 =====
export function UploadCloud({ size = 64 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/>
<polyline points="12 16 12 8"/>
<polyline points="8 12 12 8 16 12"/>
<line x1="12" y1="16" x2="12" y2="22"/>
</svg>
)
}
// ===== 箭头/导航图标 =====
export function ArrowUp({ size = 12 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"/>
<polyline points="5 12 12 5 19 12"/>
</svg>
)
}
export function ArrowDown({ size = 12 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<polyline points="19 12 12 19 5 12"/>
</svg>
)
}
// ===== 欢迎屏幕图标 =====
export function WelcomeFile({ size = 20 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" 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"/>
<line x1="12" y1="11" x2="12" y2="17"/>
<line x1="9" y1="14" x2="15" y2="14"/>
</svg>
)
}
export function WelcomeNew({ size = 20 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<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"/>
<line x1="12" y1="18" x2="12" y2="12"/>
<line x1="9" y1="15" x2="15" y2="15"/>
</svg>
)
}
@@ -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<HTMLInputElement>(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<HTMLInputElement>) => {
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() {
.*
</button>
<button className="search-nav-btn" onClick={findPrev} title="上一个 (Shift+Enter)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="18 15 12 9 6 15" />
</svg>
<ChevronUp size={12} />
</button>
<button className="search-nav-btn" onClick={findNext} title="下一个 (Enter)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="6 9 12 15 18 9" />
</svg>
<ChevronDown size={12} />
</button>
<button className="search-nav-btn" onClick={close} title="关闭 (Escape)">
<X size={14} />
</button>
<button className="search-nav-btn" onClick={close} title="关闭 (Escape)"></button>
</div>
{showReplace && (
+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>
</>
)}
+3 -14
View File
@@ -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<ContextMenuState>({ visible: false, x: 0, y: 0, tabId: '' })
const menuRef = useRef<HTMLDivElement>(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)}
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<Close size={10} />
</button>
</div>
))}
@@ -134,10 +127,7 @@ export function TabBar() {
onClick={() => createTab(null, '')}
title="新建标签页 (Ctrl+T)"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
<Plus size={14} />
</button>
{/* 右键菜单 */}
@@ -151,7 +141,6 @@ export function TabBar() {
<div className="tab-context-item" onClick={handleMenuClose}>
</div>
{/* M-03: 单标签时隐藏"关闭其他标签" */}
{tabs.length > 1 && (
<div className="tab-context-item" onClick={handleMenuCloseOthers}>
+7 -37
View File
@@ -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,
<div id="toolbar">
<div className="toolbar-left">
<button className="toolbar-btn" onClick={onOpen} title="打开文件 (Ctrl+O)">
<svg width="18" height="18" 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>
<FolderOpen size={18} />
<span></span>
</button>
<button className="toolbar-btn" onClick={onSave} title="保存文件 (Ctrl+S)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
<Save size={18} />
<span></span>
</button>
<div className="toolbar-divider" />
<button className={`toolbar-btn ${viewMode === 'split' ? 'active' : ''}`} onClick={() => onViewModeChange('split')} title="编辑+预览 (Ctrl+1)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="12" y1="3" x2="12" y2="21" />
</svg>
<SplitView size={18} />
<span></span>
</button>
<button className={`toolbar-btn ${viewMode === 'editor' ? 'active' : ''}`} onClick={() => onViewModeChange('editor')} title="纯编辑 (Ctrl+2)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
<EditMode size={18} />
<span></span>
</button>
<button className={`toolbar-btn ${viewMode === 'preview' ? 'active' : ''}`} onClick={() => onViewModeChange('preview')} title="纯预览 (Ctrl+3)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
<PreviewMode size={18} />
<span></span>
</button>
</div>
<div className="toolbar-right">
<button className="toolbar-btn" onClick={onToggleDark} title="切换暗色主题">
{darkMode ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)}
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
</button>
</div>
</div>
@@ -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) {
<div id="welcome-screen">
<div className="welcome-content">
<div className="welcome-icon">
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="5" width="80" height="90" rx="8" fill="#f0f6ff" stroke="#1a73e8" strokeWidth="2" />
<text x="50" y="45" textAnchor="middle" fill="#1a73e8" fontFamily="system-ui" fontWeight="bold" fontSize="32">M</text>
<text x="50" y="70" textAnchor="middle" fill="#5f6368" fontFamily="system-ui" fontSize="12">MarkLite</text>
</svg>
<AppIcon size={80} />
</div>
<h1>使 MarkLite</h1>
<p> Markdown </p>
<p> Markdown </p>
<div className="welcome-actions">
<button className="welcome-btn primary" onClick={onOpen}>
<svg width="20" height="20" 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>
<WelcomeFile size={20} />
</button>
<button className="welcome-btn secondary" onClick={onNew}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
<WelcomeNew size={20} />
</button>
</div>