feat: 实现笔临 P0/P1 Electron 桌面应用 v0.1.0
交付写作核心(TipTap、自动保存、分卷分章)、Onboarding、7 主题与双语 i18n、快捷键设置;主进程使用 node:sqlite;补全 Vitest 与 Playwright E2E;统一 design/ 目录并移除 desigin 拼写错误路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Provider as JotaiProvider } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './i18n'
|
||||
import './styles/themes.css'
|
||||
import './styles/globals.css'
|
||||
import './styles/layout.css'
|
||||
import { TopBar } from '@renderer/components/layout/TopBar'
|
||||
import { HomePage } from '@renderer/components/home/HomePage'
|
||||
import { EditorLayout } from '@renderer/components/layout/EditorLayout'
|
||||
import { SettingsPage } from '@renderer/components/settings/SettingsPage'
|
||||
import { OnboardingWizard } from '@renderer/components/onboarding/OnboardingWizard'
|
||||
import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
|
||||
import { useTabStore } from '@renderer/stores/useTabStore'
|
||||
import { useBookStore } from '@renderer/stores/useBookStore'
|
||||
import { flushEditorSave } from '@renderer/components/editor/TipTapEditor'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
|
||||
function AppInner(): React.JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const view = useAppStore((s) => s.view)
|
||||
const toast = useAppStore((s) => s.toast)
|
||||
const showToast = useAppStore((s) => s.showToast)
|
||||
const setView = useAppStore((s) => s.setView)
|
||||
const { loaded, onboardingCompleted, load: loadSettings } = useSettingsStore()
|
||||
const loadBooks = useBookStore((s) => s.loadBooks)
|
||||
const { activeTabId, openTabs } = useTabStore()
|
||||
const openBook = useBookStore((s) => s.openBook)
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await loadSettings()
|
||||
await loadBooks()
|
||||
})()
|
||||
}, [loadSettings, loadBooks])
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded && !onboardingCompleted) setShowOnboarding(true)
|
||||
}, [loaded, onboardingCompleted])
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = window.electronAPI.onShortcutTriggered(async (action) => {
|
||||
if (action === 'saveChapter') {
|
||||
try {
|
||||
await flushEditorSave()
|
||||
} catch {
|
||||
showToast(t('toast.saveFailed'))
|
||||
}
|
||||
return
|
||||
}
|
||||
if (action === 'newChapter') {
|
||||
if (view !== 'editor') return
|
||||
const { currentBookId, activeVolumeId, refreshChapters, setSelectedChapter } = useBookStore.getState()
|
||||
if (!currentBookId || !activeVolumeId) return
|
||||
await flushEditorSave()
|
||||
const ch = await ipcCall(() =>
|
||||
window.electronAPI.chapter.create(currentBookId, activeVolumeId, t('editor.newChapter'))
|
||||
)
|
||||
await refreshChapters()
|
||||
setSelectedChapter(ch.id)
|
||||
return
|
||||
}
|
||||
if (action === 'closeTab') {
|
||||
const tab = openTabs.find((x) => x.id === activeTabId)
|
||||
if (!tab) return
|
||||
const next = useTabStore.getState().closeTab(tab.id)
|
||||
if (next) {
|
||||
const nt = useTabStore.getState().openTabs.find((x) => x.id === next)
|
||||
if (nt?.type === 'book' && nt.bookId) {
|
||||
await openBook(nt.bookId)
|
||||
setView('editor')
|
||||
} else setView('settings')
|
||||
} else setView('home')
|
||||
return
|
||||
}
|
||||
showToast(t('feature.comingSoon'))
|
||||
})
|
||||
return unsub
|
||||
}, [view, activeTabId, openTabs, openBook, setView, showToast, t])
|
||||
|
||||
const handleOnboardingComplete = (): void => {
|
||||
setShowOnboarding(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="app">
|
||||
<TopBar />
|
||||
<div id="main-area">
|
||||
{view === 'home' && <HomePage />}
|
||||
{view === 'editor' && <EditorLayout />}
|
||||
{view === 'settings' && <SettingsPage />}
|
||||
</div>
|
||||
{showOnboarding && <OnboardingWizard onComplete={handleOnboardingComplete} />}
|
||||
{toast && <div className="toast">{toast}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App(): React.JSX.Element {
|
||||
return (
|
||||
<JotaiProvider>
|
||||
<AppInner />
|
||||
</JotaiProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user