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
@@ -0,0 +1,103 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { ThemeId, Language } from '@shared/types'
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
import { ShortcutEditor } from '@renderer/components/settings/ShortcutEditor'
const THEMES: { id: ThemeId; key: string }[] = [
{ id: 'default', key: 'theme.default' },
{ id: 'bamboo', key: 'theme.bamboo' },
{ id: 'moonlit', key: 'theme.moonlit' },
{ id: 'ricepaper', key: 'theme.ricepaper' },
{ id: 'mist', key: 'theme.mist' },
{ id: 'teagarden', key: 'theme.teagarden' },
{ id: 'twilight', key: 'theme.twilight' }
]
export function SettingsPage(): React.JSX.Element {
const { t } = useTranslation()
const settings = useSettingsStore()
const [section, setSection] = useState<'general' | 'shortcuts' | 'about'>('general')
return (
<div id="settings-page">
<div className="settings-sidebar">
<div
className={`settings-nav-item ${section === 'general' ? 'active' : ''}`}
onClick={() => setSection('general')}
role="button"
tabIndex={0}
>
{t('settings.general')}
</div>
<div
className={`settings-nav-item ${section === 'shortcuts' ? 'active' : ''}`}
onClick={() => setSection('shortcuts')}
role="button"
tabIndex={0}
>
{t('settings.shortcuts')}
</div>
<div
className={`settings-nav-item ${section === 'about' ? 'active' : ''}`}
onClick={() => setSection('about')}
role="button"
tabIndex={0}
>
{t('settings.about')}
</div>
</div>
<div className="settings-content">
<h2 style={{ marginBottom: 20 }}>{t('settings.title')}</h2>
{section === 'general' && (
<>
<div className="setting-item">
<span>{t('settings.penName')}</span>
<input
data-testid="settings-pen-name"
value={settings.penName}
onChange={(e) => void settings.update({ penName: e.target.value })}
style={{ width: 160, padding: '6px 8px', borderRadius: 4, border: '1px solid var(--border)', background: 'var(--bg-surface)', color: 'inherit' }}
/>
</div>
<div className="setting-item">
<span>{t('settings.theme')}</span>
<select
data-testid="settings-theme"
value={settings.theme}
onChange={(e) => void settings.update({ theme: e.target.value as ThemeId })}
style={{ padding: '6px 8px', borderRadius: 4, border: '1px solid var(--border)', background: 'var(--bg-surface)', color: 'inherit' }}
>
{THEMES.map((th) => (
<option key={th.id} value={th.id}>
{t(th.key)}
</option>
))}
</select>
</div>
<div className="setting-item">
<span>{t('settings.language')}</span>
<select
data-testid="settings-language"
value={settings.language}
onChange={(e) => void settings.update({ language: e.target.value as Language })}
style={{ padding: '6px 8px', borderRadius: 4, border: '1px solid var(--border)', background: 'var(--bg-surface)', color: 'inherit' }}
>
<option value="zh-CN"></option>
<option value="en">English</option>
</select>
</div>
</>
)}
{section === 'shortcuts' && <ShortcutEditor />}
{section === 'about' && (
<p style={{ color: 'var(--text-secondary)', lineHeight: 1.8 }}>
{t('app.name')} v0.1.0
<br />
{t('app.tagline')}
</p>
)}
</div>
</div>
)
}