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,138 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
import { useBookStore } from '@renderer/stores/useBookStore'
|
||||
import { useTabStore } from '@renderer/stores/useTabStore'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
|
||||
export function HomePage(): React.JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const showToast = useAppStore((s) => s.showToast)
|
||||
const setView = useAppStore((s) => s.setView)
|
||||
const { books, loadBooks, openBook } = useBookStore()
|
||||
const { openBookTab, openSettingsTab } = useTabStore()
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [category, setCategory] = useState('玄幻')
|
||||
const [target, setTarget] = useState('')
|
||||
|
||||
const handleOpenBook = async (bookId: string, bookName: string): Promise<void> => {
|
||||
await openBook(bookId)
|
||||
openBookTab(bookId, bookName)
|
||||
setView('editor')
|
||||
}
|
||||
|
||||
const handleCreate = async (): Promise<void> => {
|
||||
if (!name.trim()) {
|
||||
showToast(t('toast.enterBookName'))
|
||||
return
|
||||
}
|
||||
const meta = await ipcCall(() =>
|
||||
window.electronAPI.book.create({
|
||||
name: name.trim(),
|
||||
category,
|
||||
targetWordCount: target ? Number(target) : null
|
||||
})
|
||||
)
|
||||
setShowDialog(false)
|
||||
setName('')
|
||||
await loadBooks()
|
||||
showToast(t('toast.bookCreated'))
|
||||
await handleOpenBook(meta.id, meta.name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="home-page">
|
||||
<div className="home-hero">
|
||||
<h1>✒ {t('app.name')}</h1>
|
||||
<p className="subtitle">{t('app.tagline')}</p>
|
||||
</div>
|
||||
<div className="home-actions">
|
||||
<button type="button" className="btn-large primary" onClick={() => setShowDialog(true)}>
|
||||
+ {t('home.newBook')}
|
||||
</button>
|
||||
<button type="button" className="btn-large" onClick={() => showToast(t('feature.comingSoon'))}>
|
||||
{t('home.importBook')}
|
||||
</button>
|
||||
<button type="button" className="btn-large" onClick={() => showToast(t('feature.comingSoon'))}>
|
||||
{t('home.exportAll')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-large"
|
||||
onClick={() => {
|
||||
openSettingsTab()
|
||||
setView('settings')
|
||||
}}
|
||||
>
|
||||
⚙ {t('settings.title')}
|
||||
</button>
|
||||
</div>
|
||||
<h3 style={{ marginBottom: 12, fontSize: 14 }}>
|
||||
📚 {t('home.myBooks')}{' '}
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 12 }}>
|
||||
{t('home.bookCount', { count: books.length })}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="book-grid">
|
||||
{books.map((book) => (
|
||||
<div
|
||||
key={book.id}
|
||||
className="book-card"
|
||||
onClick={() => void handleOpenBook(book.id, book.name)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && void handleOpenBook(book.id, book.name)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="book-name">{book.name}</div>
|
||||
<div className="book-meta">
|
||||
<div>{book.category}</div>
|
||||
<div>{new Date(book.lastOpenedAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="book-card"
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 100, color: 'var(--text-muted)' }}
|
||||
onClick={() => setShowDialog(true)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
+ {t('home.newBook')}
|
||||
</div>
|
||||
</div>
|
||||
{showDialog && (
|
||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||
<div className="dialog-content">
|
||||
<h3 style={{ marginBottom: 16 }}>{t('dialog.newBook.title')}</h3>
|
||||
<div className="form-group">
|
||||
<label>{t('dialog.newBook.name')}</label>
|
||||
<input value={name} onChange={(e) => setName(e.target.value)} autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('dialog.newBook.category')}</label>
|
||||
<select value={category} onChange={(e) => setCategory(e.target.value)}>
|
||||
<option value="玄幻">玄幻</option>
|
||||
<option value="仙侠">仙侠</option>
|
||||
<option value="科幻">科幻</option>
|
||||
<option value="言情">言情</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('dialog.newBook.target')}</label>
|
||||
<input type="number" value={target} onChange={(e) => setTarget(e.target.value)} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
|
||||
<button type="button" className="btn" onClick={() => setShowDialog(false)}>
|
||||
{t('dialog.cancel')}
|
||||
</button>
|
||||
<button type="button" className="btn primary" onClick={() => void handleCreate()}>
|
||||
{t('dialog.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user