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:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { UploadCloud } from '../Icons'
|
||||||
|
|
||||||
export function DropOverlay() {
|
export function DropOverlay() {
|
||||||
// L-11: 简化为单一状态
|
|
||||||
const [dragCounter, setDragCounter] = useState(0)
|
const [dragCounter, setDragCounter] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,9 +41,7 @@ export function DropOverlay() {
|
|||||||
return (
|
return (
|
||||||
<div id="drop-overlay">
|
<div id="drop-overlay">
|
||||||
<div className="drop-content">
|
<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">
|
<UploadCloud size={64} />
|
||||||
<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>
|
|
||||||
<p>释放文件以打开</p>
|
<p>释放文件以打开</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { useSearchStore } from '../../stores/searchStore'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import { findMatches } from '../../lib/searchEngine'
|
import { findMatches } from '../../lib/searchEngine'
|
||||||
|
import { ChevronUp, ChevronDown, X } from '../Icons'
|
||||||
|
|
||||||
export function SearchBar() {
|
export function SearchBar() {
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -25,7 +26,6 @@ export function SearchBar() {
|
|||||||
|
|
||||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||||
|
|
||||||
// 执行搜索
|
|
||||||
const doSearch = useCallback((text: string) => {
|
const doSearch = useCallback((text: string) => {
|
||||||
const tab = getActiveTab()
|
const tab = getActiveTab()
|
||||||
if (!tab || !text) {
|
if (!tab || !text) {
|
||||||
@@ -36,21 +36,18 @@ export function SearchBar() {
|
|||||||
setMatches(found)
|
setMatches(found)
|
||||||
}, [getActiveTab, options, setMatches])
|
}, [getActiveTab, options, setMatches])
|
||||||
|
|
||||||
// 搜索文本变化
|
|
||||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const text = e.target.value
|
const text = e.target.value
|
||||||
setSearchText(text)
|
setSearchText(text)
|
||||||
doSearch(text)
|
doSearch(text)
|
||||||
}, [setSearchText, doSearch])
|
}, [setSearchText, doSearch])
|
||||||
|
|
||||||
// 替换当前
|
|
||||||
const handleReplaceCurrent = useCallback(() => {
|
const handleReplaceCurrent = useCallback(() => {
|
||||||
const tab = getActiveTab()
|
const tab = getActiveTab()
|
||||||
if (!tab || matches.length === 0 || currentIndex < 0) return
|
if (!tab || matches.length === 0 || currentIndex < 0) return
|
||||||
const m = matches[currentIndex]
|
const m = matches[currentIndex]
|
||||||
const newContent = tab.content.substring(0, m.start) + replaceText + tab.content.substring(m.end)
|
const newContent = tab.content.substring(0, m.start) + replaceText + tab.content.substring(m.end)
|
||||||
useTabStore.getState().updateTabContent(tab.id, newContent)
|
useTabStore.getState().updateTabContent(tab.id, newContent)
|
||||||
// 重新搜索
|
|
||||||
doSearch(searchText)
|
doSearch(searchText)
|
||||||
}, [getActiveTab, matches, currentIndex, replaceText, searchText, doSearch])
|
}, [getActiveTab, matches, currentIndex, replaceText, searchText, doSearch])
|
||||||
|
|
||||||
@@ -67,7 +64,6 @@ export function SearchBar() {
|
|||||||
doSearch(searchText)
|
doSearch(searchText)
|
||||||
}, [getActiveTab, matches, replaceText, searchText, doSearch])
|
}, [getActiveTab, matches, replaceText, searchText, doSearch])
|
||||||
|
|
||||||
// 打开时聚焦
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible && searchInputRef.current) {
|
if (isVisible && searchInputRef.current) {
|
||||||
searchInputRef.current.focus()
|
searchInputRef.current.focus()
|
||||||
@@ -123,16 +119,14 @@ export function SearchBar() {
|
|||||||
.*
|
.*
|
||||||
</button>
|
</button>
|
||||||
<button className="search-nav-btn" onClick={findPrev} title="上一个 (Shift+Enter)">
|
<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">
|
<ChevronUp size={12} />
|
||||||
<polyline points="18 15 12 9 6 15" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<button className="search-nav-btn" onClick={findNext} title="下一个 (Enter)">
|
<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">
|
<ChevronDown size={12} />
|
||||||
<polyline points="6 9 12 15 18 9" />
|
</button>
|
||||||
</svg>
|
<button className="search-nav-btn" onClick={close} title="关闭 (Escape)">
|
||||||
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button className="search-nav-btn" onClick={close} title="关闭 (Escape)">✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showReplace && (
|
{showReplace && (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useCallback, useState, useRef } from 'react'
|
|||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import { useSidebarStore } from '../../stores/sidebarStore'
|
import { useSidebarStore } from '../../stores/sidebarStore'
|
||||||
import { getFileName } from '../../lib/fileUtils'
|
import { getFileName } from '../../lib/fileUtils'
|
||||||
|
import { FolderPlus, File, Folder, ChevronRight } from '../Icons'
|
||||||
import type { FileNode } from '../../types/file'
|
import type { FileNode } from '../../types/file'
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
@@ -19,22 +20,19 @@ export function Sidebar() {
|
|||||||
const isVisible = useSidebarStore(s => s.isVisible)
|
const isVisible = useSidebarStore(s => s.isVisible)
|
||||||
const expandDirs = useSidebarStore(s => s.expandDirs)
|
const expandDirs = useSidebarStore(s => s.expandDirs)
|
||||||
|
|
||||||
// M-01: 归一化路径分隔符(Windows 混合 \\ 和 /)
|
// M-01: 归一化路径分隔符
|
||||||
const norm = (p: string) => p.replace(/\\/g, '/')
|
const norm = (p: string) => p.replace(/\\/g, '/')
|
||||||
const activeFilePath = tabs.find(t => t.id === activeTabId)?.filePath ?? null
|
const activeFilePath = tabs.find(t => t.id === activeTabId)?.filePath ?? null
|
||||||
|
|
||||||
// 自动展开到活动文件所在的目录
|
// 自动展开到活动文件所在的目录
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeFilePath || !rootPath) return
|
if (!activeFilePath || !rootPath) return
|
||||||
// M-01: 归一化后比较
|
|
||||||
const normActive = norm(activeFilePath)
|
const normActive = norm(activeFilePath)
|
||||||
const normRoot = norm(rootPath)
|
const normRoot = norm(rootPath)
|
||||||
if (!normActive.startsWith(normRoot)) return
|
if (!normActive.startsWith(normRoot)) return
|
||||||
|
|
||||||
// 提取文件的所有父目录路径
|
|
||||||
const dirsToExpand: string[] = []
|
const dirsToExpand: string[] = []
|
||||||
let dir = activeFilePath.replace(/[/\\][^/\\]+$/, '') // 移除文件名
|
let dir = activeFilePath.replace(/[/\\][^/\\]+$/, '')
|
||||||
// M-02: 添加 prev 防护无限循环
|
|
||||||
let prev = ''
|
let prev = ''
|
||||||
while (dir && dir.length >= rootPath.length && dir !== rootPath && dir !== prev) {
|
while (dir && dir.length >= rootPath.length && dir !== rootPath && dir !== prev) {
|
||||||
prev = dir
|
prev = dir
|
||||||
@@ -50,7 +48,6 @@ export function Sidebar() {
|
|||||||
const [isResizing, setIsResizing] = useState(false)
|
const [isResizing, setIsResizing] = useState(false)
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null)
|
const sidebarRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 打开文件夹
|
|
||||||
const handleOpenFolder = useCallback(async () => {
|
const handleOpenFolder = useCallback(async () => {
|
||||||
if (!window.electronAPI) return
|
if (!window.electronAPI) return
|
||||||
const dirPath = await window.electronAPI.openFolderDialog()
|
const dirPath = await window.electronAPI.openFolderDialog()
|
||||||
@@ -64,7 +61,6 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [setRootPath, setTree])
|
}, [setRootPath, setTree])
|
||||||
|
|
||||||
// 刷新目录树
|
|
||||||
const refreshTree = useCallback(async () => {
|
const refreshTree = useCallback(async () => {
|
||||||
if (!rootPath || !window.electronAPI) return
|
if (!rootPath || !window.electronAPI) return
|
||||||
const result = await window.electronAPI.readDirTree(rootPath)
|
const result = await window.electronAPI.readDirTree(rootPath)
|
||||||
@@ -73,7 +69,6 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [rootPath, setTree])
|
}, [rootPath, setTree])
|
||||||
|
|
||||||
// 目录变化监听
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.electronAPI) return
|
if (!window.electronAPI) return
|
||||||
window.electronAPI.onDirChanged(() => {
|
window.electronAPI.onDirChanged(() => {
|
||||||
@@ -84,7 +79,6 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [refreshTree])
|
}, [refreshTree])
|
||||||
|
|
||||||
// 侧边栏宽度调节
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isResizing) return
|
if (!isResizing) return
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
@@ -102,7 +96,6 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [isResizing])
|
}, [isResizing])
|
||||||
|
|
||||||
// L-12: 提取为 useCallback 避免每次渲染重建
|
|
||||||
const handleFileClick = useCallback(async (path: string) => {
|
const handleFileClick = useCallback(async (path: string) => {
|
||||||
const existing = tabs.find(t => t.filePath === path)
|
const existing = tabs.find(t => t.filePath === path)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -114,6 +107,7 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [tabs, switchToTab, createTab])
|
}, [tabs, switchToTab, createTab])
|
||||||
|
|
||||||
// M-01: 独立文件区(归一化路径比较)
|
// M-01: 独立文件区(归一化路径比较)
|
||||||
const independentFiles = tabs.filter(t => {
|
const independentFiles = tabs.filter(t => {
|
||||||
if (!t.filePath) return false
|
if (!t.filePath) return false
|
||||||
@@ -128,9 +122,7 @@ export function Sidebar() {
|
|||||||
<div id="sidebar-header">
|
<div id="sidebar-header">
|
||||||
<span id="sidebar-title">资源管理器</span>
|
<span id="sidebar-title">资源管理器</span>
|
||||||
<button className="sidebar-header-btn" onClick={handleOpenFolder} title="打开文件夹">
|
<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">
|
<FolderPlus size={14} />
|
||||||
<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>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,10 +139,7 @@ export function Sidebar() {
|
|||||||
onClick={() => switchToTab(tab.id)}
|
onClick={() => switchToTab(tab.id)}
|
||||||
>
|
>
|
||||||
<span className="tree-icon">
|
<span className="tree-icon">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<File size={14} />
|
||||||
<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>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="tree-name">{getFileName(tab.filePath!)}</span>
|
<span className="tree-name">{getFileName(tab.filePath!)}</span>
|
||||||
{tab.isModified && <span className="independent-modified-dot"> •</span>}
|
{tab.isModified && <span className="independent-modified-dot"> •</span>}
|
||||||
@@ -176,7 +165,6 @@ export function Sidebar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 调节手柄 */}
|
|
||||||
<div
|
<div
|
||||||
className="sidebar-resize-handle"
|
className="sidebar-resize-handle"
|
||||||
onMouseDown={() => setIsResizing(true)}
|
onMouseDown={() => setIsResizing(true)}
|
||||||
@@ -213,24 +201,17 @@ 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.has(node.path) ? 'expanded' : ''}`}>
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
<ChevronRight size={10} />
|
||||||
<polyline points="9 18 15 12 9 6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="tree-icon">
|
<span className="tree-icon">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<Folder size={14} />
|
||||||
<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>
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span style={{ width: '16px', flexShrink: 0 }} />
|
<span style={{ width: '16px', flexShrink: 0 }} />
|
||||||
<span className="tree-icon">
|
<span className="tree-icon">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<File size={14} />
|
||||||
<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>
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useState, useEffect, useRef } from 'react'
|
import React, { useCallback, useState, useEffect, useRef } from 'react'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import { getFileName } from '../../lib/fileUtils'
|
import { getFileName } from '../../lib/fileUtils'
|
||||||
|
import { Close, Plus } from '../Icons'
|
||||||
|
|
||||||
interface ContextMenuState {
|
interface ContextMenuState {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -22,7 +23,6 @@ export function TabBar() {
|
|||||||
const [menu, setMenu] = useState<ContextMenuState>({ visible: false, x: 0, y: 0, tabId: '' })
|
const [menu, setMenu] = useState<ContextMenuState>({ visible: false, x: 0, y: 0, tabId: '' })
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 关闭标签(带未保存确认)
|
|
||||||
const handleClose = useCallback((e: React.MouseEvent, tabId: string) => {
|
const handleClose = useCallback((e: React.MouseEvent, tabId: string) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const tab = tabs.find(t => t.id === tabId)
|
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 })
|
setMenu({ visible: true, x: Math.max(0, x), y: Math.max(0, y), tabId })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 点击空白处关闭菜单
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!menu.visible) return
|
if (!menu.visible) return
|
||||||
const handleClick = () => setMenu(prev => ({ ...prev, visible: false }))
|
const handleClick = () => setMenu(prev => ({ ...prev, visible: false }))
|
||||||
@@ -52,7 +51,6 @@ export function TabBar() {
|
|||||||
return () => document.removeEventListener('click', handleClick)
|
return () => document.removeEventListener('click', handleClick)
|
||||||
}, [menu.visible])
|
}, [menu.visible])
|
||||||
|
|
||||||
// 菜单操作
|
|
||||||
const handleMenuClose = useCallback(() => {
|
const handleMenuClose = useCallback(() => {
|
||||||
const tab = tabs.find(t => t.id === menu.tabId)
|
const tab = tabs.find(t => t.id === menu.tabId)
|
||||||
if (tab?.isModified) {
|
if (tab?.isModified) {
|
||||||
@@ -64,7 +62,6 @@ export function TabBar() {
|
|||||||
}, [tabs, menu.tabId, closeTab])
|
}, [tabs, menu.tabId, closeTab])
|
||||||
|
|
||||||
const handleMenuCloseOthers = useCallback(() => {
|
const handleMenuCloseOthers = useCallback(() => {
|
||||||
// 检查其他标签是否有未保存
|
|
||||||
const otherModified = tabs.filter(t => t.id !== menu.tabId && t.isModified)
|
const otherModified = tabs.filter(t => t.id !== menu.tabId && t.isModified)
|
||||||
if (otherModified.length > 0) {
|
if (otherModified.length > 0) {
|
||||||
const names = otherModified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
|
const names = otherModified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
|
||||||
@@ -96,7 +93,6 @@ export function TabBar() {
|
|||||||
setMenu(prev => ({ ...prev, visible: false }))
|
setMenu(prev => ({ ...prev, visible: false }))
|
||||||
}, [tabs, menu.tabId, closeTabsToRight])
|
}, [tabs, menu.tabId, closeTabsToRight])
|
||||||
|
|
||||||
// 判断右侧是否还有标签
|
|
||||||
const hasRightTabs = menu.visible && (() => {
|
const hasRightTabs = menu.visible && (() => {
|
||||||
const index = tabs.findIndex(t => t.id === menu.tabId)
|
const index = tabs.findIndex(t => t.id === menu.tabId)
|
||||||
return index < tabs.length - 1
|
return index < tabs.length - 1
|
||||||
@@ -121,10 +117,7 @@ export function TabBar() {
|
|||||||
className="tab-close"
|
className="tab-close"
|
||||||
onClick={(e) => handleClose(e, tab.id)}
|
onClick={(e) => handleClose(e, tab.id)}
|
||||||
>
|
>
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<Close size={10} />
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -134,10 +127,7 @@ export function TabBar() {
|
|||||||
onClick={() => createTab(null, '')}
|
onClick={() => createTab(null, '')}
|
||||||
title="新建标签页 (Ctrl+T)"
|
title="新建标签页 (Ctrl+T)"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<Plus size={14} />
|
||||||
<line x1="12" y1="5" x2="12" y2="19" />
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 右键菜单 */}
|
{/* 右键菜单 */}
|
||||||
@@ -151,7 +141,6 @@ export function TabBar() {
|
|||||||
<div className="tab-context-item" onClick={handleMenuClose}>
|
<div className="tab-context-item" onClick={handleMenuClose}>
|
||||||
关闭
|
关闭
|
||||||
</div>
|
</div>
|
||||||
{/* M-03: 单标签时隐藏"关闭其他标签" */}
|
|
||||||
{tabs.length > 1 && (
|
{tabs.length > 1 && (
|
||||||
<div className="tab-context-item" onClick={handleMenuCloseOthers}>
|
<div className="tab-context-item" onClick={handleMenuCloseOthers}>
|
||||||
关闭其他标签
|
关闭其他标签
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { FolderOpen, Save, SplitView, EditMode, PreviewMode, Moon, Sun } from '../Icons'
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
onOpen: () => void
|
onOpen: () => void
|
||||||
@@ -14,61 +15,30 @@ export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode,
|
|||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<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)">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<FolderOpen size={18} />
|
||||||
<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>
|
|
||||||
<span>打开</span>
|
<span>打开</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="toolbar-btn" onClick={onSave} title="保存文件 (Ctrl+S)">
|
<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">
|
<Save size={18} />
|
||||||
<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>
|
|
||||||
<span>保存</span>
|
<span>保存</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="toolbar-divider" />
|
<div className="toolbar-divider" />
|
||||||
<button className={`toolbar-btn ${viewMode === 'split' ? 'active' : ''}`} onClick={() => onViewModeChange('split')} title="编辑+预览 (Ctrl+1)">
|
<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">
|
<SplitView size={18} />
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
||||||
<line x1="12" y1="3" x2="12" y2="21" />
|
|
||||||
</svg>
|
|
||||||
<span>分屏</span>
|
<span>分屏</span>
|
||||||
</button>
|
</button>
|
||||||
<button className={`toolbar-btn ${viewMode === 'editor' ? 'active' : ''}`} onClick={() => onViewModeChange('editor')} title="纯编辑 (Ctrl+2)">
|
<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">
|
<EditMode size={18} />
|
||||||
<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>
|
|
||||||
<span>编辑</span>
|
<span>编辑</span>
|
||||||
</button>
|
</button>
|
||||||
<button className={`toolbar-btn ${viewMode === 'preview' ? 'active' : ''}`} onClick={() => onViewModeChange('preview')} title="纯预览 (Ctrl+3)">
|
<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">
|
<PreviewMode size={18} />
|
||||||
<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>
|
|
||||||
<span>预览</span>
|
<span>预览</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbar-right">
|
<div className="toolbar-right">
|
||||||
<button className="toolbar-btn" onClick={onToggleDark} title="切换暗色主题">
|
<button className="toolbar-btn" onClick={onToggleDark} title="切换暗色主题">
|
||||||
{darkMode ? (
|
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { AppIcon, WelcomeFile, WelcomeNew } from '../Icons'
|
||||||
|
|
||||||
interface WelcomeScreenProps {
|
interface WelcomeScreenProps {
|
||||||
onOpen: () => void
|
onOpen: () => void
|
||||||
@@ -10,26 +11,17 @@ export function WelcomeScreen({ onOpen, onNew }: WelcomeScreenProps) {
|
|||||||
<div id="welcome-screen">
|
<div id="welcome-screen">
|
||||||
<div className="welcome-content">
|
<div className="welcome-content">
|
||||||
<div className="welcome-icon">
|
<div className="welcome-icon">
|
||||||
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<AppIcon size={80} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<h1>欢迎使用 MarkLite</h1>
|
<h1>欢迎使用 MarkLite</h1>
|
||||||
<p>一款轻量级的 Markdown 阅读器</p>
|
<p>一款轻量级的 Markdown 编辑器</p>
|
||||||
<div className="welcome-actions">
|
<div className="welcome-actions">
|
||||||
<button className="welcome-btn primary" onClick={onOpen}>
|
<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">
|
<WelcomeFile size={20} />
|
||||||
<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>
|
|
||||||
打开文件
|
打开文件
|
||||||
</button>
|
</button>
|
||||||
<button className="welcome-btn secondary" onClick={onNew}>
|
<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">
|
<WelcomeNew size={20} />
|
||||||
<line x1="12" y1="5" x2="12" y2="19" />
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
|
||||||
</svg>
|
|
||||||
新建文件
|
新建文件
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user