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:
2026-07-06 10:39:20 +08:00
parent a06038035b
commit 011bd6d4ca
70 changed files with 14245 additions and 84 deletions
+107
View File
@@ -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>
)
}