fix: 修复侧边栏根目录、搜索高亮导航、行号同步
问题1: 侧边栏打开文件夹后根目录不显示
修复: FileTree 传入包装后的根节点 { name, path, type: 'dir', children: tree }
打开文件夹时自动展开根目录 expandDirs([rootPath])
问题2: 搜索没有高亮,上一个/下一个没反应
修复: Editor 添加完整搜索功能
- 搜索时在 textarea 上方创建 overlay div
- 用绝对定位 div 标记每个匹配位置(单行/多行)
- currentIndex 变化时选中匹配文本并滚动到可视区域
- 搜索结果随内容变化自动更新
问题3: 滚动时行号跟随不准确
修复: 行高测量改为与 CSS line-height 一致的动态测量
搜索高亮位置随编辑器滚动同步更新
This commit is contained in:
@@ -1,28 +1,34 @@
|
|||||||
import React, { useEffect, useRef, useCallback, useState } from 'react'
|
import React, { useEffect, useRef, useCallback, useState } from 'react'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import { useSearchStore } from '../../stores/searchStore'
|
import { useSearchStore } from '../../stores/searchStore'
|
||||||
|
import { findMatches, buildLineStarts, getLineNumber } from '../../lib/searchEngine'
|
||||||
|
|
||||||
export function Editor() {
|
export function Editor() {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const lineNumbersRef = useRef<HTMLDivElement>(null)
|
const lineNumbersRef = useRef<HTMLDivElement>(null)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const highlightRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const activeTabId = useTabStore(s => s.activeTabId)
|
const activeTabId = useTabStore(s => s.activeTabId)
|
||||||
|
const tabs = useTabStore(s => s.tabs)
|
||||||
|
const activeTab = tabs.find(t => t.id === activeTabId) ?? null
|
||||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||||
const updateTabContent = useTabStore(s => s.updateTabContent)
|
const updateTabContent = useTabStore(s => s.updateTabContent)
|
||||||
const setModified = useTabStore(s => s.setModified)
|
const setModified = useTabStore(s => s.setModified)
|
||||||
|
|
||||||
// L-13: 只选择需要的字段,避免整个 store 变化触发重渲染
|
// 搜索状态
|
||||||
const searchIsVisible = useSearchStore(s => s.isVisible)
|
const searchIsVisible = useSearchStore(s => s.isVisible)
|
||||||
const searchFindNext = useSearchStore(s => s.findNext)
|
const searchText = useSearchStore(s => s.searchText)
|
||||||
const searchFindPrev = useSearchStore(s => s.findPrev)
|
const searchMatches = useSearchStore(s => s.matches)
|
||||||
|
const searchCurrentIndex = useSearchStore(s => s.currentIndex)
|
||||||
|
const searchOptions = useSearchStore(s => s.options)
|
||||||
|
|
||||||
const [lineCount, setLineCount] = useState(1)
|
const [lineCount, setLineCount] = useState(1)
|
||||||
const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 })
|
const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 })
|
||||||
const [fileSize, setFileSize] = useState('')
|
const [fileSize, setFileSize] = useState('')
|
||||||
const updateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const updateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
// 测量行高
|
// 行高
|
||||||
const lineHeightRef = useRef(20.8)
|
const lineHeightRef = useRef(20.8)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current
|
||||||
@@ -39,64 +45,41 @@ export function Editor() {
|
|||||||
lineHeightRef.current = doubleHeight - singleHeight || 20.8
|
lineHeightRef.current = doubleHeight - singleHeight || 20.8
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 加载标签内容到编辑器
|
// 字符宽度
|
||||||
|
const charWidthRef = useRef(8)
|
||||||
|
useEffect(() => {
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (!textarea) return
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
const style = getComputedStyle(textarea)
|
||||||
|
ctx.font = `${style.fontSize} ${style.fontFamily}`
|
||||||
|
charWidthRef.current = ctx.measureText('M').width
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 加载标签内容
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tab = getActiveTab()
|
const tab = getActiveTab()
|
||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current
|
||||||
if (!tab || !textarea) return
|
if (!tab || !textarea) return
|
||||||
|
|
||||||
textarea.value = tab.content
|
textarea.value = tab.content
|
||||||
updateLineNumbers(tab.content)
|
setLineCount((tab.content.match(/\n/g) || []).length + 1)
|
||||||
updateFileSizeDisplay(tab.content)
|
const bytes = new Blob([tab.content]).size
|
||||||
updateCursorPos()
|
setFileSize(bytes < 1024 ? bytes + ' B' : bytes < 1048576 ? (bytes / 1024).toFixed(1) + ' KB' : (bytes / 1048576).toFixed(1) + ' MB')
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
textarea.scrollTop = tab.scrollTop
|
textarea.scrollTop = tab.scrollTop
|
||||||
textarea.scrollLeft = tab.scrollLeft
|
textarea.scrollLeft = tab.scrollLeft
|
||||||
textarea.setSelectionRange(tab.selectionStart, tab.selectionEnd)
|
textarea.setSelectionRange(tab.selectionStart, tab.selectionEnd)
|
||||||
if (lineNumbersRef.current) {
|
if (lineNumbersRef.current) lineNumbersRef.current.scrollTop = tab.scrollTop
|
||||||
lineNumbersRef.current.scrollTop = tab.scrollTop
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}, [activeTabId, getActiveTab])
|
}, [activeTabId, getActiveTab])
|
||||||
|
|
||||||
// 保存当前标签状态
|
|
||||||
const saveCurrentState = useCallback(() => {
|
|
||||||
const tab = getActiveTab()
|
|
||||||
const textarea = textareaRef.current
|
|
||||||
if (!tab || !textarea) return
|
|
||||||
useTabStore.getState().updateTabScroll(tab.id, {
|
|
||||||
scrollTop: textarea.scrollTop,
|
|
||||||
scrollLeft: textarea.scrollLeft,
|
|
||||||
selectionStart: textarea.selectionStart,
|
|
||||||
selectionEnd: textarea.selectionEnd,
|
|
||||||
previewScrollTop: 0 // will be set by preview
|
|
||||||
})
|
|
||||||
}, [getActiveTab])
|
|
||||||
|
|
||||||
// 更新行号
|
// 更新行号
|
||||||
const updateLineNumbers = useCallback((content: string) => {
|
const updateLineNumbers = useCallback((content: string) => {
|
||||||
const count = (content.match(/\n/g) || []).length + 1
|
setLineCount((content.match(/\n/g) || []).length + 1)
|
||||||
setLineCount(count)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 更新文件大小
|
|
||||||
const updateFileSizeDisplay = useCallback((content: string) => {
|
|
||||||
const bytes = new Blob([content]).size
|
|
||||||
if (bytes < 1024) setFileSize(bytes + ' B')
|
|
||||||
else if (bytes < 1024 * 1024) setFileSize((bytes / 1024).toFixed(1) + ' KB')
|
|
||||||
else setFileSize((bytes / (1024 * 1024)).toFixed(1) + ' MB')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 更新光标位置
|
|
||||||
const updateCursorPos = useCallback(() => {
|
|
||||||
const textarea = textareaRef.current
|
|
||||||
if (!textarea) return
|
|
||||||
const text = textarea.value
|
|
||||||
const pos = textarea.selectionStart
|
|
||||||
const textBefore = text.substring(0, pos)
|
|
||||||
const lines = textBefore.split('\n')
|
|
||||||
setCursorPos({ line: lines.length, col: lines[lines.length - 1].length + 1 })
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 输入事件
|
// 输入事件
|
||||||
@@ -104,35 +87,142 @@ export function Editor() {
|
|||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current
|
||||||
const tab = getActiveTab()
|
const tab = getActiveTab()
|
||||||
if (!textarea || !tab) return
|
if (!textarea || !tab) return
|
||||||
|
|
||||||
const content = textarea.value
|
const content = textarea.value
|
||||||
updateTabContent(tab.id, content)
|
updateTabContent(tab.id, content)
|
||||||
setModified(tab.id, true)
|
setModified(tab.id, true)
|
||||||
updateLineNumbers(content)
|
updateLineNumbers(content)
|
||||||
updateFileSizeDisplay(content)
|
const bytes = new Blob([content]).size
|
||||||
|
setFileSize(bytes < 1024 ? bytes + ' B' : bytes < 1048576 ? (bytes / 1024).toFixed(1) + ' KB' : (bytes / 1048576).toFixed(1) + ' MB')
|
||||||
|
}, [getActiveTab, updateTabContent, setModified, updateLineNumbers])
|
||||||
|
|
||||||
// 防抖更新
|
// 滚动同步
|
||||||
if (updateTimerRef.current) clearTimeout(updateTimerRef.current)
|
|
||||||
updateTimerRef.current = setTimeout(() => {
|
|
||||||
// 触发预览更新(通过 store)
|
|
||||||
}, 150)
|
|
||||||
}, [getActiveTab, updateTabContent, setModified, updateLineNumbers, updateFileSizeDisplay])
|
|
||||||
|
|
||||||
// 滚动同步:通知 Preview 跟随滚动
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current
|
||||||
if (!textarea) return
|
if (!textarea) return
|
||||||
// 行号跟随
|
|
||||||
if (lineNumbersRef.current) {
|
if (lineNumbersRef.current) {
|
||||||
lineNumbersRef.current.scrollTop = textarea.scrollTop
|
lineNumbersRef.current.scrollTop = textarea.scrollTop
|
||||||
}
|
}
|
||||||
// 计算滚动比例,派发给 Preview
|
// 更新搜索高亮位置
|
||||||
|
if (highlightRef.current) {
|
||||||
|
highlightRef.current.style.top = -textarea.scrollTop + 'px'
|
||||||
|
}
|
||||||
const maxScroll = textarea.scrollHeight - textarea.clientHeight
|
const maxScroll = textarea.scrollHeight - textarea.clientHeight
|
||||||
const ratio = maxScroll > 0 ? textarea.scrollTop / maxScroll : 0
|
const ratio = maxScroll > 0 ? textarea.scrollTop / maxScroll : 0
|
||||||
window.dispatchEvent(new CustomEvent('editor-scroll', { detail: { ratio } }))
|
window.dispatchEvent(new CustomEvent('editor-scroll', { detail: { ratio } }))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Tab 键支持
|
// 搜索:执行搜索并更新 matches
|
||||||
|
const activeContent = activeTab?.content
|
||||||
|
useEffect(() => {
|
||||||
|
if (!searchIsVisible || !searchText) {
|
||||||
|
if (highlightRef.current) highlightRef.current.innerHTML = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (!textarea) return
|
||||||
|
|
||||||
|
const matches = findMatches(textarea.value, searchText, searchOptions)
|
||||||
|
useSearchStore.getState().setMatches(matches)
|
||||||
|
renderHighlights(textarea.value, matches, searchCurrentIndex)
|
||||||
|
}, [searchIsVisible, searchText, searchOptions, activeContent])
|
||||||
|
|
||||||
|
// 搜索:currentIndex 变化时滚动到匹配位置
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchCurrentIndex < 0 || searchMatches.length === 0) return
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (!textarea) return
|
||||||
|
|
||||||
|
const m = searchMatches[searchCurrentIndex]
|
||||||
|
if (!m) return
|
||||||
|
|
||||||
|
// 选中匹配文本
|
||||||
|
textarea.focus()
|
||||||
|
textarea.setSelectionRange(m.start, m.end)
|
||||||
|
|
||||||
|
// 滚动到匹配位置
|
||||||
|
const lineStarts = buildLineStarts(textarea.value)
|
||||||
|
const line = getLineNumber(lineStarts, m.start)
|
||||||
|
const lineHeight = lineHeightRef.current
|
||||||
|
const matchTop = line * lineHeight
|
||||||
|
const viewTop = textarea.scrollTop
|
||||||
|
const viewBottom = viewTop + textarea.clientHeight
|
||||||
|
if (matchTop < viewTop + 40 || matchTop > viewBottom - 40) {
|
||||||
|
textarea.scrollTop = matchTop - textarea.clientHeight / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新渲染高亮
|
||||||
|
renderHighlights(textarea.value, searchMatches, searchCurrentIndex)
|
||||||
|
}, [searchCurrentIndex, searchMatches])
|
||||||
|
|
||||||
|
// 渲染搜索高亮 overlay
|
||||||
|
const renderHighlights = useCallback((content: string, matches: Array<{start: number; end: number}>, currentIndex: number) => {
|
||||||
|
const wrapper = wrapperRef.current
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (!wrapper || !textarea) return
|
||||||
|
|
||||||
|
// 创建或复用 overlay
|
||||||
|
if (!highlightRef.current) {
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.id = 'search-highlights'
|
||||||
|
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;height:' + textarea.scrollHeight + 'px;pointer-events:none;overflow:visible;z-index:1;'
|
||||||
|
wrapper.style.position = 'relative'
|
||||||
|
wrapper.appendChild(overlay)
|
||||||
|
highlightRef.current = overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = highlightRef.current
|
||||||
|
overlay.style.top = -textarea.scrollTop + 'px'
|
||||||
|
overlay.style.height = textarea.scrollHeight + 'px'
|
||||||
|
overlay.innerHTML = ''
|
||||||
|
|
||||||
|
if (matches.length === 0) return
|
||||||
|
|
||||||
|
const style = getComputedStyle(textarea)
|
||||||
|
const paddingTop = parseFloat(style.paddingTop)
|
||||||
|
const paddingLeft = parseFloat(style.paddingLeft)
|
||||||
|
const lineHeight = lineHeightRef.current
|
||||||
|
const charWidth = charWidthRef.current
|
||||||
|
const lineStarts = buildLineStarts(content)
|
||||||
|
|
||||||
|
for (let i = 0; i < matches.length; i++) {
|
||||||
|
const m = matches[i]
|
||||||
|
const startLine = getLineNumber(lineStarts, m.start)
|
||||||
|
const startCol = m.start - lineStarts[startLine]
|
||||||
|
const endLine = getLineNumber(lineStarts, m.end)
|
||||||
|
const endCol = m.end - lineStarts[endLine]
|
||||||
|
|
||||||
|
if (startLine === endLine) {
|
||||||
|
// 单行匹配
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.className = 'search-hl' + (i === currentIndex ? ' current' : '')
|
||||||
|
div.style.cssText = `position:absolute;top:${startLine * lineHeight + paddingTop}px;left:${startCol * charWidth + paddingLeft}px;height:${lineHeight}px;width:${Math.max((endCol - startCol) * charWidth, 8)}px;`
|
||||||
|
overlay.appendChild(div)
|
||||||
|
} else {
|
||||||
|
// 多行匹配:第一行
|
||||||
|
const div1 = document.createElement('div')
|
||||||
|
div1.className = 'search-hl' + (i === currentIndex ? ' current' : '')
|
||||||
|
const firstLineWidth = textarea.clientWidth - (startCol * charWidth + paddingLeft) - 20
|
||||||
|
div1.style.cssText = `position:absolute;top:${startLine * lineHeight + paddingTop}px;left:${startCol * charWidth + paddingLeft}px;height:${lineHeight}px;width:${firstLineWidth}px;`
|
||||||
|
overlay.appendChild(div1)
|
||||||
|
|
||||||
|
// 中间行
|
||||||
|
for (let l = startLine + 1; l < endLine; l++) {
|
||||||
|
const divM = document.createElement('div')
|
||||||
|
divM.className = 'search-hl' + (i === currentIndex ? ' current' : '')
|
||||||
|
divM.style.cssText = `position:absolute;top:${l * lineHeight + paddingTop}px;left:${paddingLeft}px;height:${lineHeight}px;width:${textarea.clientWidth - paddingLeft * 2}px;`
|
||||||
|
overlay.appendChild(divM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后一行
|
||||||
|
const div2 = document.createElement('div')
|
||||||
|
div2.className = 'search-hl' + (i === currentIndex ? ' current' : '')
|
||||||
|
div2.style.cssText = `position:absolute;top:${endLine * lineHeight + paddingTop}px;left:${paddingLeft}px;height:${lineHeight}px;width:${endCol * charWidth}px;`
|
||||||
|
overlay.appendChild(div2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Tab 键
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -148,45 +238,28 @@ export function Editor() {
|
|||||||
document.execCommand('insertText', false, ' ')
|
document.execCommand('insertText', false, ' ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索导航
|
|
||||||
if (e.key === 'Enter' && searchIsVisible) {
|
if (e.key === 'Enter' && searchIsVisible) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (e.shiftKey) {
|
const state = useSearchStore.getState()
|
||||||
searchFindPrev()
|
if (e.shiftKey) state.findPrev()
|
||||||
} else {
|
else state.findNext()
|
||||||
searchFindNext()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [searchIsVisible, searchFindNext, searchFindPrev])
|
}, [searchIsVisible])
|
||||||
|
|
||||||
|
// 更新光标位置
|
||||||
|
const updateCursorPos = useCallback(() => {
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (!textarea) return
|
||||||
|
const text = textarea.value
|
||||||
|
const pos = textarea.selectionStart
|
||||||
|
const textBefore = text.substring(0, pos)
|
||||||
|
const lines = textBefore.split('\n')
|
||||||
|
setCursorPos({ line: lines.length, col: lines[lines.length - 1].length + 1 })
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 渲染行号
|
// 渲染行号
|
||||||
const renderLineNumbers = () => {
|
const renderLineNumbers = () => {
|
||||||
const lineHeight = lineHeightRef.current
|
const lineHeight = lineHeightRef.current
|
||||||
const LARGE_THRESHOLD = 2000
|
|
||||||
if (lineCount > LARGE_THRESHOLD) {
|
|
||||||
// 虚拟化
|
|
||||||
const textarea = textareaRef.current
|
|
||||||
if (!textarea) return null
|
|
||||||
const scrollTop = textarea.scrollTop
|
|
||||||
const viewportHeight = textarea.clientHeight
|
|
||||||
const buffer = 20
|
|
||||||
const startLine = Math.max(0, Math.floor(scrollTop / lineHeight) - buffer)
|
|
||||||
const endLine = Math.min(lineCount, Math.ceil((scrollTop + viewportHeight) / lineHeight) + buffer)
|
|
||||||
const topPadding = startLine * lineHeight
|
|
||||||
const bottomPadding = (lineCount - endLine) * lineHeight
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div style={{ height: topPadding }} />
|
|
||||||
{Array.from({ length: endLine - startLine }, (_, i) => (
|
|
||||||
<div key={startLine + i} className="line-num" style={{ height: lineHeight }}>
|
|
||||||
{startLine + i + 1}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div style={{ height: bottomPadding }} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return Array.from({ length: lineCount }, (_, i) => (
|
return Array.from({ length: lineCount }, (_, i) => (
|
||||||
<div key={i} className="line-num" style={{ height: lineHeight }}>
|
<div key={i} className="line-num" style={{ height: lineHeight }}>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
@@ -194,6 +267,16 @@ export function Editor() {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理 overlay
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (highlightRef.current) {
|
||||||
|
highlightRef.current.remove()
|
||||||
|
highlightRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="editor-wrapper" ref={wrapperRef}>
|
<div id="editor-wrapper" ref={wrapperRef}>
|
||||||
<div id="line-numbers" ref={lineNumbersRef}>
|
<div id="line-numbers" ref={lineNumbersRef}>
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export function SearchBar() {
|
|||||||
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)
|
// Editor 会监听 searchText 变化并执行搜索和高亮
|
||||||
}, [setSearchText, doSearch])
|
}, [setSearchText])
|
||||||
|
|
||||||
const handleReplaceCurrent = useCallback(() => {
|
const handleReplaceCurrent = useCallback(() => {
|
||||||
const tab = getActiveTab()
|
const tab = getActiveTab()
|
||||||
|
|||||||
@@ -53,13 +53,15 @@ export function Sidebar() {
|
|||||||
const dirPath = await window.electronAPI.openFolderDialog()
|
const dirPath = await window.electronAPI.openFolderDialog()
|
||||||
if (dirPath) {
|
if (dirPath) {
|
||||||
setRootPath(dirPath)
|
setRootPath(dirPath)
|
||||||
|
// 展开根目录
|
||||||
|
expandDirs([dirPath])
|
||||||
const result = await window.electronAPI.readDirTree(dirPath)
|
const result = await window.electronAPI.readDirTree(dirPath)
|
||||||
if (result.success && result.tree) {
|
if (result.success && result.tree) {
|
||||||
setTree(result.tree)
|
setTree(result.tree)
|
||||||
window.electronAPI.watchDir(dirPath)
|
window.electronAPI.watchDir(dirPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [setRootPath, setTree])
|
}, [setRootPath, setTree, expandDirs])
|
||||||
|
|
||||||
const refreshTree = useCallback(async () => {
|
const refreshTree = useCallback(async () => {
|
||||||
if (!rootPath || !window.electronAPI) return
|
if (!rootPath || !window.electronAPI) return
|
||||||
@@ -153,7 +155,7 @@ export function Sidebar() {
|
|||||||
<>
|
<>
|
||||||
<div className="sidebar-section-header">文件夹目录树</div>
|
<div className="sidebar-section-header">文件夹目录树</div>
|
||||||
<FileTree
|
<FileTree
|
||||||
nodes={tree}
|
nodes={[{ name: rootPath.split(/[/\\]/).pop() || rootPath, path: rootPath, type: 'dir' as const, children: tree }]}
|
||||||
depth={0}
|
depth={0}
|
||||||
expandedDirs={expandedDirs}
|
expandedDirs={expandedDirs}
|
||||||
toggleDir={toggleDir}
|
toggleDir={toggleDir}
|
||||||
|
|||||||
Reference in New Issue
Block a user