011bd6d4ca
交付写作核心(TipTap、自动保存、分卷分章)、Onboarding、7 主题与双语 i18n、快捷键设置;主进程使用 node:sqlite;补全 Vitest 与 Playwright E2E;统一 design/ 目录并移除 desigin 拼写错误路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
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>
|
|
)
|
|
}
|