chore: bump version to 0.3.11 — 状态栏扩展点架构
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useAutoSaveStore } from '../stores/autoSaveStore'
|
||||
import { logError } from '../lib/errorHandler'
|
||||
|
||||
/** Debounce delay for auto-save (ms) */
|
||||
const AUTO_SAVE_DELAY = 2000
|
||||
|
||||
/** 模块级 ref — 状态栏等外部组件通过此函数切换自动保存 */
|
||||
let _toggleAutoSave: (() => void) | null = null
|
||||
export function toggleAutoSaveExternal(): void {
|
||||
_toggleAutoSave?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-save hook: subscribes to Zustand tab store, debounces content changes
|
||||
* and saves automatically after a pause in editing for file-backed tabs.
|
||||
@@ -36,6 +43,9 @@ export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 暴露给外部(状态栏按钮)
|
||||
_toggleAutoSave = toggleAutoSave
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
|
||||
@@ -93,5 +103,14 @@ export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 同步 autoSaveEnabled / isAutoSaving 到 autoSaveStore(供状态栏读取)
|
||||
useEffect(() => {
|
||||
useAutoSaveStore.getState().setState({ autoSaveEnabled })
|
||||
}, [autoSaveEnabled])
|
||||
|
||||
useEffect(() => {
|
||||
useAutoSaveStore.getState().setState({ isAutoSaving })
|
||||
}, [isAutoSaving])
|
||||
|
||||
return { isAutoSaving, autoSaveEnabled, toggleAutoSave }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useStatusBarStore, type StatusBarItem } from '../stores/statusBarStore'
|
||||
|
||||
/**
|
||||
* 便捷 hook — 挂载时注册一个状态栏项,卸载时自动清除。
|
||||
*
|
||||
* 适合在 App.tsx 或自定义 hook 中注册需要响应式更新的状态栏项。
|
||||
* 当 item 对象变化时(通过 deps 控制),旧项会被新项替换。
|
||||
*
|
||||
* @param item - 状态栏项(含 Component 函数组件)
|
||||
*/
|
||||
export function useStatusBarItem(item: StatusBarItem | null): void {
|
||||
const register = useStatusBarStore(s => s.register)
|
||||
const unregister = useStatusBarStore(s => s.unregister)
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) return
|
||||
register(item)
|
||||
return () => unregister(item.id)
|
||||
}, [item, register, unregister])
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useStatusBarStore } from '../stores/statusBarStore'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import { useDocStats } from './useDocStats'
|
||||
import { getFileName } from '../lib/fileUtils'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner/LoadingSpinner'
|
||||
|
||||
/** 文件信息 — 文件名或"就绪" */
|
||||
function FileInfoItem() {
|
||||
const activeTab = useTabStore(s => s.getActiveTab())
|
||||
return (
|
||||
<span id="status-text">
|
||||
{activeTab ? (activeTab.filePath ? getFileName(activeTab.filePath) : '未命名') : '就绪'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 加载指示器 — 打开文件/加载目录/渲染中 */
|
||||
function LoadingItem() {
|
||||
const loadingStates = useEditorStore(s => s.loadingStates)
|
||||
const hasLoading = Object.values(loadingStates).some(Boolean)
|
||||
if (!hasLoading) return null
|
||||
|
||||
const label = loadingStates['file-open']
|
||||
? '正在打开文件...'
|
||||
: loadingStates['dir-load']
|
||||
? '正在加载目录...'
|
||||
: loadingStates['markdown-render']
|
||||
? '正在渲染...'
|
||||
: ''
|
||||
|
||||
return (
|
||||
<span className="status-loading" aria-busy="true">
|
||||
<LoadingSpinner size="small" />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 文档统计 — 行数 / 词数 / 字符数 */
|
||||
function DocStatsItem() {
|
||||
const activeTab = useTabStore(s => s.getActiveTab())
|
||||
const stats = useDocStats(activeTab?.content)
|
||||
if (!activeTab) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="status-stat" title="行数" aria-label={`${stats.lines} 行`}>{stats.lines} 行</span>
|
||||
<span className="status-divider">|</span>
|
||||
<span className="status-stat" title="单词数" aria-label={`${stats.words} 个单词`}>{stats.words} 词</span>
|
||||
<span className="status-divider">|</span>
|
||||
<span className="status-stat" title="字符数(含空格)" aria-label={`${stats.chars} 个字符`}>{stats.chars} 字符</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 静态项 — 编码 */
|
||||
function EncodingItem() {
|
||||
return <span id="status-encoding">UTF-8</span>
|
||||
}
|
||||
|
||||
/** 静态项 — 语言 */
|
||||
function LangItem() {
|
||||
return <span id="status-lang">Markdown</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* useDefaultStatusBarItems — 注册所有默认状态栏项(不含 auto-save)。
|
||||
*
|
||||
* auto-save 项由 App.tsx 单独注册,因为它需要从 autoSaveStore
|
||||
* 读取状态并通过 toggleAutoSaveExternal() 触发切换。
|
||||
*/
|
||||
export function useDefaultStatusBarItems() {
|
||||
const register = useStatusBarStore(s => s.register)
|
||||
const unregister = useStatusBarStore(s => s.unregister)
|
||||
|
||||
useEffect(() => {
|
||||
register({ id: 'statusbar.file-info', alignment: 'left', priority: 0, Component: FileInfoItem })
|
||||
register({ id: 'statusbar.loading', alignment: 'left', priority: 1, Component: LoadingItem })
|
||||
register({ id: 'statusbar.doc-stats', alignment: 'right', priority: 100, Component: DocStatsItem })
|
||||
register({ id: 'statusbar.encoding', alignment: 'right', priority: 300, Component: EncodingItem })
|
||||
register({ id: 'statusbar.lang', alignment: 'right', priority: 400, Component: LangItem })
|
||||
|
||||
return () => {
|
||||
unregister('statusbar.file-info')
|
||||
unregister('statusbar.loading')
|
||||
unregister('statusbar.doc-stats')
|
||||
unregister('statusbar.encoding')
|
||||
unregister('statusbar.lang')
|
||||
}
|
||||
}, [register, unregister])
|
||||
}
|
||||
Reference in New Issue
Block a user