From f707b5c7ee4c6eece44df9d681e73062e1fe70ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=BD=B1233?= <1440196015@qq.com> Date: Tue, 23 Jun 2026 11:36:46 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20bump=20version=20to=200.3.11=20?= =?UTF-8?q?=E2=80=94=20=E7=8A=B6=E6=80=81=E6=A0=8F=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E7=82=B9=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DESIGN.md | 2 +- README.md | 2 +- package.json | 2 +- src/renderer/App.tsx | 37 ++++++- .../components/StatusBar/StatusBar.tsx | 96 ++++++------------- src/renderer/hooks/useAutoSave.ts | 19 ++++ src/renderer/hooks/useStatusBarItem.ts | 21 ++++ src/renderer/hooks/useStatusBarItems.tsx | 93 ++++++++++++++++++ src/renderer/stores/autoSaveStore.ts | 24 +++++ src/renderer/stores/statusBarStore.ts | 47 +++++++++ src/shared/constants.ts | 2 +- 11 files changed, 270 insertions(+), 75 deletions(-) create mode 100644 src/renderer/hooks/useStatusBarItem.ts create mode 100644 src/renderer/hooks/useStatusBarItems.tsx create mode 100644 src/renderer/stores/autoSaveStore.ts create mode 100644 src/renderer/stores/statusBarStore.ts diff --git a/DESIGN.md b/DESIGN.md index 9fbe047..42f4387 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,4 +1,4 @@ -# MarkLite v0.3.10 — 架构设计文档 +# MarkLite v0.3.11 — 架构设计文档 ## 1. 项目概述 diff --git a/README.md b/README.md index 09139ab..f57f734 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ TypeScript React License - Version + Version

diff --git a/package.json b/package.json index e09e776..0e3af01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marklite", - "version": "0.3.10", + "version": "0.3.11", "description": "Lightweight Markdown Editor for Windows", "main": "./dist/main/index.js", "scripts": { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c5896dc..cba24b2 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -14,6 +14,10 @@ import { useAutoSave } from './hooks/useAutoSave' import { useIpcListeners } from './hooks/useIpcListeners' import { useToast } from './hooks/useToast' import { useConfirm } from './hooks/useConfirm' +import { useDefaultStatusBarItems } from './hooks/useStatusBarItems' +import { useStatusBarItem } from './hooks/useStatusBarItem' +import { useAutoSaveStore } from './stores/autoSaveStore' +import { toggleAutoSaveExternal } from './hooks/useAutoSave' import { Toolbar } from './components/Toolbar/Toolbar' import { TabBar } from './components/TabBar/TabBar' import { Editor } from './components/Editor/Editor' @@ -29,6 +33,25 @@ import { ErrorBoundary } from './components/ErrorBoundary' import { AboutDialog } from './components/AboutDialog' import { ConfirmDialog } from './components/ConfirmDialog/ConfirmDialog' +/** 状态栏 — 自动保存开关组件 */ +function AutoSaveStatusItem() { + const activeTab = useTabStore(s => s.getActiveTab()) + const { isAutoSaving, autoSaveEnabled } = useAutoSaveStore() + + if (!activeTab?.filePath) return null + + return ( + + ) +} + export function App() { const tabs = useTabStore(s => s.tabs) const activeTabId = useTabStore(s => s.activeTabId) @@ -52,7 +75,7 @@ export function App() { const { handleOpenFile, handleSave, handleSaveAs, handleOpenRecent } = useFileOperations(showToast) useDragDrop(showToast) useFileWatch() - const { isAutoSaving, autoSaveEnabled, toggleAutoSave } = useAutoSave() + useAutoSave() // UX-01: 传入 confirm 函数替代原生 confirm() const handleConfirmClose = useCallback(async (message: string): Promise => { @@ -69,6 +92,16 @@ export function App() { useKeyboard(handleOpenFile, handleSave, handleSaveAs) useIpcListeners() + // ── 状态栏扩展点架构 ─────────────────────────────────── + useDefaultStatusBarItems() + + useStatusBarItem({ + id: 'statusbar.auto-save', + alignment: 'right', + priority: 200, + Component: AutoSaveStatusItem + }) + useEffect(() => { if (!window.electronAPI) return const activeTab = tabs.find(t => t.id === activeTabId) @@ -118,7 +151,7 @@ export function App() { )} - + {showAbout && } diff --git a/src/renderer/components/StatusBar/StatusBar.tsx b/src/renderer/components/StatusBar/StatusBar.tsx index 25f4376..c4fdfb2 100644 --- a/src/renderer/components/StatusBar/StatusBar.tsx +++ b/src/renderer/components/StatusBar/StatusBar.tsx @@ -1,81 +1,39 @@ -import React from 'react' -import { useTabStore } from '../../stores/tabStore' -import { useEditorStore } from '../../stores/editorStore' -import { useDocStats } from '../../hooks/useDocStats' -import { getFileName } from '../../lib/fileUtils' -import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner' +import React, { useMemo } from 'react' +import { useStatusBarStore, type StatusBarItem } from '../../stores/statusBarStore' -interface StatusBarProps { - isAutoSaving?: boolean - autoSaveEnabled?: boolean - onToggleAutoSave?: () => void -} +/** + * StatusBar — 纯渲染层 + * + * 不包含任何业务逻辑。所有状态栏项由各功能模块通过 + * useStatusBarStore.register() 注入,本组件按 alignment + priority 排序渲染。 + * 每个 item.Component 是独立 React 组件,可自由使用 hooks。 + */ -export const StatusBar = React.memo(function StatusBar({ - isAutoSaving = false, - autoSaveEnabled = true, - onToggleAutoSave -}: StatusBarProps) { - const activeTab = useTabStore(s => s.getActiveTab()) - const loadingStates = useEditorStore(s => s.loadingStates) - const stats = useDocStats(activeTab?.content) +export const StatusBar = React.memo(function StatusBar() { + const items = useStatusBarStore(s => s.items) - // 判断是否有任何加载状态激活 - const hasLoading = Object.values(loadingStates).some(Boolean) - const loadingLabel = loadingStates['file-open'] - ? '正在打开文件...' - : loadingStates['dir-load'] - ? '正在加载目录...' - : loadingStates['markdown-render'] - ? '正在渲染...' - : '' + const sorted = useMemo(() => { + const list = Object.values(items) + if (list.length === 0) return { left: [] as StatusBarItem[], right: [] as StatusBarItem[] } + const byAlignment = (a: 'left' | 'right') => + list.filter(i => i.alignment === a).sort((x, y) => x.priority - y.priority) + return { left: byAlignment('left'), right: byAlignment('right') } + }, [items]) return (

- {hasLoading && ( - - - {loadingLabel} - - )} - - {activeTab ? (activeTab.filePath ? getFileName(activeTab.filePath) : '未命名') : '就绪'} - + {sorted.left.map(({ id, Component }) => ( + + ))}
- {activeTab && ( - <> - - {stats.lines} 行 - - | - - {stats.words} 词 - - | - - {stats.chars} 字符 - - | - - )} - {activeTab?.filePath && onToggleAutoSave !== undefined && ( - <> - - | - - )} - UTF-8 - | - Markdown + {sorted.right.map(({ id, Component }, i) => ( + + {i > 0 && |} + + + ))}
) diff --git a/src/renderer/hooks/useAutoSave.ts b/src/renderer/hooks/useAutoSave.ts index 156347f..9eb0ec0 100644 --- a/src/renderer/hooks/useAutoSave.ts +++ b/src/renderer/hooks/useAutoSave.ts @@ -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 } } diff --git a/src/renderer/hooks/useStatusBarItem.ts b/src/renderer/hooks/useStatusBarItem.ts new file mode 100644 index 0000000..0b31e04 --- /dev/null +++ b/src/renderer/hooks/useStatusBarItem.ts @@ -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]) +} diff --git a/src/renderer/hooks/useStatusBarItems.tsx b/src/renderer/hooks/useStatusBarItems.tsx new file mode 100644 index 0000000..d4fec3b --- /dev/null +++ b/src/renderer/hooks/useStatusBarItems.tsx @@ -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 ( + + {activeTab ? (activeTab.filePath ? getFileName(activeTab.filePath) : '未命名') : '就绪'} + + ) +} + +/** 加载指示器 — 打开文件/加载目录/渲染中 */ +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 ( + + + {label} + + ) +} + +/** 文档统计 — 行数 / 词数 / 字符数 */ +function DocStatsItem() { + const activeTab = useTabStore(s => s.getActiveTab()) + const stats = useDocStats(activeTab?.content) + if (!activeTab) return null + + return ( + <> + {stats.lines} 行 + | + {stats.words} 词 + | + {stats.chars} 字符 + + ) +} + +/** 静态项 — 编码 */ +function EncodingItem() { + return UTF-8 +} + +/** 静态项 — 语言 */ +function LangItem() { + return Markdown +} + +/** + * 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]) +} diff --git a/src/renderer/stores/autoSaveStore.ts b/src/renderer/stores/autoSaveStore.ts new file mode 100644 index 0000000..80e6b0a --- /dev/null +++ b/src/renderer/stores/autoSaveStore.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' + +/** + * 轻量 auto-save 状态 store + * + * 解耦 useAutoSave hook 与状态栏组件——状态栏的 auto-save 项通过 + * 本 store 读取状态,无需在 render 函数中重复调用 useAutoSave()。 + * useAutoSave hook 负责写入本 store。 + */ + +interface AutoSaveState { + isAutoSaving: boolean + autoSaveEnabled: boolean +} + +interface AutoSaveStore extends AutoSaveState { + setState: (partial: Partial) => void +} + +export const useAutoSaveStore = create((set) => ({ + isAutoSaving: false, + autoSaveEnabled: true, + setState: (partial) => set(partial) +})) diff --git a/src/renderer/stores/statusBarStore.ts b/src/renderer/stores/statusBarStore.ts new file mode 100644 index 0000000..e836c76 --- /dev/null +++ b/src/renderer/stores/statusBarStore.ts @@ -0,0 +1,47 @@ +import { create } from 'zustand' +import type { ComponentType } from 'react' + +/** + * 状态栏扩展点架构 + * + * 功能模块通过 register/unregister 注入自己的状态栏组件, + * StatusBar 只负责按 alignment + priority 排序渲染。 + * 避免 StatusBar 随功能膨胀,实现类似 VS Code contributions 的注册模式。 + * + * 每个 StatusBarItem 的 Component 是独立的 React 函数组件(无 props), + * 内部使用 hooks 读取所需 store 状态。组件在 StatusBar 的渲染中挂载, + * 因此 hooks 调用在合法的 React 组件上下文中。 + */ + +export interface StatusBarItem { + id: string + alignment: 'left' | 'right' + /** 同侧排序权重,数字越小越靠左(left)或越靠中(right) */ + priority: number + Component: ComponentType +} + +interface StatusBarState { + /** 以 id 为 key,方便覆盖和删除 */ + items: Record + register: (item: StatusBarItem) => void + unregister: (id: string) => void +} + +export const useStatusBarStore = create((set, get) => ({ + items: {}, + + register: (item: StatusBarItem) => { + const { items } = get() + if (items[item.id]?.Component === item.Component) return + set({ items: { ...items, [item.id]: item } }) + }, + + unregister: (id: string) => { + const { items } = get() + if (!(id in items)) return + const next = { ...items } + delete next[id] + set({ items: next }) + } +})) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index cbfc5b4..05ff5dc 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,5 +1,5 @@ // 共享常量 — 主进程和渲染进程共用 -export const APP_VERSION = 'v0.3.10' +export const APP_VERSION = 'v0.3.11' export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB export const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const export const SKIP_DIRS = new Set([