v0.3.7: 全面代码审计修复 - 20项Bug/安全/稳定性改进
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import { AppIcon, Gitee } from '../Icons'
|
||||
|
||||
const APP_VERSION = 'v0.3.6'
|
||||
import { APP_VERSION } from '../../lib/constants'
|
||||
|
||||
interface AboutDialogProps {
|
||||
onClose: () => void
|
||||
|
||||
@@ -27,7 +27,10 @@ export const ConfirmDialog = React.memo(function ConfirmDialog({
|
||||
// 打开时保存焦点并聚焦确认按钮;关闭时恢复焦点
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
previousFocusRef.current?.focus()
|
||||
// Only restore focus if the element is still in the DOM
|
||||
if (previousFocusRef.current && previousFocusRef.current.isConnected) {
|
||||
previousFocusRef.current.focus()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -44,16 +44,39 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
||||
if (!activeTab) return
|
||||
if (activeTab.content !== currentContentRef.current) {
|
||||
currentContentRef.current = activeTab.content
|
||||
setContent(activeTab.content)
|
||||
// Queue setContent after Milkdown editor has finished async create()
|
||||
requestAnimationFrame(() => {
|
||||
setScrollTop(activeTab.scrollTop)
|
||||
setSelection()
|
||||
setContent(activeTab.content)
|
||||
requestAnimationFrame(() => {
|
||||
setScrollTop(activeTab.scrollTop)
|
||||
setSelection()
|
||||
})
|
||||
})
|
||||
}
|
||||
// stable refs: setContent, setScrollTop, setSelection are useCallback([]) - never change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTabId])
|
||||
|
||||
// Save current tab state on unmount or tab switch
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!activeTabId) return
|
||||
// Snapshot the current editor state before the new tab replaces content
|
||||
const scrollPos = getScrollTop()
|
||||
const sel = getSelection()
|
||||
// Use a microtask to ensure we save before React re-renders the new tab content
|
||||
Promise.resolve().then(() => {
|
||||
updateTabScroll(activeTabId, {
|
||||
scrollTop: scrollPos,
|
||||
selectionStart: sel.from,
|
||||
selectionEnd: sel.to
|
||||
})
|
||||
})
|
||||
}
|
||||
// stable refs: getScrollTop, getSelection (useCallback([])), updateTabScroll (zustand) - never change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTabId])
|
||||
|
||||
// Register getView for OutlinePanel navigation
|
||||
useEffect(() => {
|
||||
setEditorViewGetter(getView)
|
||||
|
||||
@@ -35,6 +35,8 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
const isDev = (typeof import.meta !== 'undefined' && (import.meta as { env?: { DEV?: boolean } }).env?.DEV) ?? false
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -51,19 +53,21 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
<h2 style={{ marginBottom: '1rem', color: '#e74c3c' }}>
|
||||
应用遇到了错误
|
||||
</h2>
|
||||
<pre
|
||||
style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '600px',
|
||||
overflow: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{this.state.error?.message}
|
||||
</pre>
|
||||
{isDev && (
|
||||
<pre
|
||||
style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '600px',
|
||||
overflow: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{this.state.error?.message}
|
||||
</pre>
|
||||
)}
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
style={{
|
||||
|
||||
@@ -7,7 +7,6 @@ interface FileTreeProps {
|
||||
depth: number
|
||||
expandedDirs: string[]
|
||||
toggleDir: (path: string) => void
|
||||
activeTabId: string | null
|
||||
activeFilePath: string | null
|
||||
onFileClick: (path: string) => void
|
||||
}
|
||||
@@ -81,7 +80,6 @@ export const FileTree = React.memo(function FileTree({
|
||||
depth={depth + 1}
|
||||
expandedDirs={expandedDirs}
|
||||
toggleDir={toggleDir}
|
||||
activeTabId={null}
|
||||
activeFilePath={activeFilePath}
|
||||
onFileClick={onFileClick}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Heading } from '../OutlinePanel'
|
||||
import { getEditorView, useEditorStore } from '../../stores/editorStore'
|
||||
import { TextSelection } from '@milkdown/prose/state'
|
||||
|
||||
const norm = (p: string) => p.replace(/\\/g, '/')
|
||||
const norm = (p: string) => p.replace(/[/\\]+$/, '').replace(/\\/g, '/')
|
||||
|
||||
export const Sidebar = React.memo(function Sidebar() {
|
||||
const tabs = useTabStore(s => s.tabs)
|
||||
@@ -138,7 +138,7 @@ export const Sidebar = React.memo(function Sidebar() {
|
||||
<FileTree
|
||||
nodes={[{ name: rootPath.split(/[/\\]/).pop() || rootPath, path: rootPath, type: 'dir' as const, children: tree }]}
|
||||
depth={0} expandedDirs={expandedDirs} toggleDir={toggleDir}
|
||||
activeTabId={activeTabId} activeFilePath={activeFilePath} onFileClick={handleFileClick}
|
||||
activeFilePath={activeFilePath} onFileClick={handleFileClick}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,8 @@ export const SourceEditor = React.memo(function SourceEditor({ darkMode }: Sourc
|
||||
}, [activeTabId, updateTabContent, setModified])
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const isCtrl = e.ctrlKey || e.metaKey
|
||||
|
||||
// Tab 插入两个空格而非跳转焦点
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
@@ -39,6 +41,40 @@ export const SourceEditor = React.memo(function SourceEditor({ darkMode }: Sourc
|
||||
setModified(activeTabId, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+B: 粗体
|
||||
if (isCtrl && e.key === 'b') {
|
||||
e.preventDefault()
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea || !activeTabId) return
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const value = textarea.value
|
||||
const selected = value.substring(start, end)
|
||||
const newValue = value.substring(0, start) + '**' + selected + '**' + value.substring(end)
|
||||
textarea.value = newValue
|
||||
textarea.selectionStart = start + 2
|
||||
textarea.selectionEnd = end + 2
|
||||
updateTabContent(activeTabId, newValue)
|
||||
setModified(activeTabId, true)
|
||||
}
|
||||
|
||||
// Ctrl+I: 斜体
|
||||
if (isCtrl && e.key === 'i') {
|
||||
e.preventDefault()
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea || !activeTabId) return
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const value = textarea.value
|
||||
const selected = value.substring(start, end)
|
||||
const newValue = value.substring(0, start) + '*' + selected + '*' + value.substring(end)
|
||||
textarea.value = newValue
|
||||
textarea.selectionStart = start + 1
|
||||
textarea.selectionEnd = end + 1
|
||||
updateTabContent(activeTabId, newValue)
|
||||
setModified(activeTabId, true)
|
||||
}
|
||||
}, [activeTabId, updateTabContent, setModified])
|
||||
|
||||
return (
|
||||
|
||||
@@ -224,6 +224,7 @@ export const TabBar = React.memo(function TabBar() {
|
||||
role="menu"
|
||||
aria-label="标签操作"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="tab-context-item" role="menuitem" onClick={handleMenuClose}>
|
||||
关闭
|
||||
|
||||
Reference in New Issue
Block a user