mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 13:31:09 +08:00
feat(web): shelf and library paint a cover-grid skeleton while loading
Both screens showed an empty page with a floating "正在加载…" until the first response. The .book-grid.is-skeleton styles (same grid and density as the real one, a shimmer that goes flat under reduced motion) were already in reader.css from the legacy shelf but nothing rendered them; BookGridSkeleton does, so the first paint fills in instead of jumping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a634e9e955
commit
7fbf9dbbf0
3 files changed
+21
-2
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { BookOpen, FileText, Folder, Plus, Search, Upload } from 'lucide-react';
|
||||
import { coverTint } from '../../../ui/cover-tint.ts';
|
||||
import { BookGridSkeleton } from '../../../ui/book-grid-skeleton.tsx';
|
||||
import { ActionMenu } from '../../../ui/action-menu.tsx';
|
||||
import { Callout, Icon } from '../../../ui/toolkit.tsx';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -49,7 +50,7 @@ export function LibraryBrowsePage() {
|
||||
{listing.data && listing.data.crumbs.length > 1 && <div className="manager-crumbs library-path" aria-label="当前文件夹">{listing.data.crumbs.map((crumb, index) => <span key={`${crumb.path}-${index}`}>{index > 0 && <span className="manager-crumb-sep">/</span>}<button type="button" className="manager-crumb" onClick={() => navigate(crumb.path)}>{crumb.name}</button></span>)}</div>}
|
||||
<main className="manager-body library-browse-body">
|
||||
<form className="shelf-search library-search" role="search" onSubmit={(event: FormEvent) => { event.preventDefault(); navigate(path, 1, draft.trim()); }}><Search className="search-glyph" size={18} aria-hidden="true" /><input type="search" aria-label="搜索书库" placeholder="在书库中搜索书名、作者" value={draft} onChange={event => setDraft(event.currentTarget.value)} />{draft && <button type="button" className="search-clear" aria-label="清除搜索" onClick={() => { setDraft(''); navigate(path); }}>×</button>}</form>
|
||||
{books.isPending && <FloatingNotice message="正在加载书库…" busy />}{books.error && <Callout tone="error" title="书库加载失败">{books.error.message}</Callout>}
|
||||
{books.isPending && <><FloatingNotice message="正在加载书库…" busy /><BookGridSkeleton className="library-browse-grid" density="cozy" /></>}{books.error && <Callout tone="error" title="书库加载失败">{books.error.message}</Callout>}
|
||||
{empty && <div className="empty-state collection-empty"><BookOpen className="empty-glyph" aria-hidden="true" /><p>{search ? '没有匹配的书' : '书库还没有书'}</p><p className="muted">{search ? '换个关键词,或者检查一下作者名的写法。' : '前往文件管理添加书籍,再回到这里浏览。'}</p><div className="empty-actions">{search ? <button type="button" className="button" onClick={() => { setDraft(''); navigate(path); }}>清除搜索</button> : role === 'admin' ? <Link className="button primary" to="/library/files"><Folder size={16} />文件管理</Link> : null}</div></div>}
|
||||
{books.data && books.data.items.length > 0 && <div className="book-grid library-browse-grid" data-density="cozy" data-show-author="true">{books.data.items.map(book => <div className="book-card is-row" key={book.id}><Link className="book-open" to={`/book/${encodeURIComponent(book.id)}`} aria-label={`${book.title} ${book.author ?? ''}`}><Cover book={book} /><div className="title">{book.title || '未命名'}</div><div className="author">{book.author || '未知作者'}</div></Link>{book.shelfState === 'off' && <button type="button" className="book-shelve" aria-label={`把${book.title}加入书架`} disabled={busy === book.id} onClick={() => void toggle(book)}>+ 加入书架</button>}</div>)}</div>}
|
||||
{books.data && <div className="manager-status muted">{search ? `找到 ${count} 本` : `${count} 本 · 第 ${page} / ${pages} 页`}</div>}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { coverTint } from '../../../ui/cover-tint.ts';
|
||||
import { BookGridSkeleton } from '../../../ui/book-grid-skeleton.tsx';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { BookOpen, LibraryBig, MoreHorizontal, Search } from 'lucide-react';
|
||||
@@ -143,7 +144,7 @@ export function ShelfPage() {
|
||||
</div>
|
||||
<FloatingNotice message={actionError} error />
|
||||
{(!query.isPending && (cards.length > 0 || queryText || filtered)) && <div className="shelf-toolbar"><span className="shelf-count muted">{queryText ? `找到 ${query.data?.total ?? 0} 本` : `共 ${query.data?.total ?? 0} 本`}</span><div className="shelf-sort" role="group" aria-label="排序方式">{SHELF_SORTS.map(option => <button key={option.value} type="button" className="chip" aria-pressed={option.value === sort} onClick={() => onSort(option.value)}>{option.label}</button>)}</div></div>}
|
||||
{query.isPending && <FloatingNotice message="正在加载书架…" busy />}
|
||||
{query.isPending && <><FloatingNotice message="正在加载书架…" busy /><BookGridSkeleton density={settings.shelfDensity} /></>}
|
||||
{query.error && <Callout tone="error" title="书架加载失败">{query.error.message}</Callout>}
|
||||
{query.data && cards.length === 0 && filtered && !queryText && <div className="empty-state collection-empty"><BookOpen className="empty-glyph" aria-hidden="true" /><p>{status ? `还没有${readingLabels[status]}的书` : '这个分组里没有书'}</p><p className="muted">{status === 'want' ? '在书籍的“⋯”菜单里可以标记想读。' : '换一个分类,或查看全部书籍。'}</p><div className="empty-actions"><button type="button" className="button" onClick={clearFilters}>查看全部书籍</button></div></div>}
|
||||
{query.data && cards.length === 0 && !(filtered && !queryText) && <div className="empty-state collection-empty"><BookOpen className="empty-glyph" aria-hidden="true" /><p>{queryText ? '没有匹配的书' : '书架还没有书'}</p><p className="muted">{queryText ? '换个关键词,或者检查一下作者名的写法' : '从书库挑选喜欢的书,或通过书源搜索,加入书架后就能开始阅读。'}</p><div className="empty-actions">{queryText ? <button type="button" className="button" onClick={clearSearch}>清除搜索</button> : <><Link className="button primary" to="/sources"><Search size={16} />去搜书</Link><Link className="button" to="/library"><LibraryBig size={16} />打开书库</Link></>}</div></div>}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* The shelf's shape before the books arrive (styles: `.book-grid.is-skeleton`).
|
||||
*
|
||||
* Same grid, same density as the real one, so the first paint is filled in rather
|
||||
* than replaced: a skeleton in a different grid makes the reader watch the layout
|
||||
* move twice. Hidden from assistive tech — the FloatingNotice beside it announces
|
||||
* the load.
|
||||
*/
|
||||
export function BookGridSkeleton({ count = 12, density, className }: { count?: number; density?: string; className?: string }) {
|
||||
return <div className={`book-grid is-skeleton${className ? ` ${className}` : ''}`} data-density={density} aria-hidden="true">
|
||||
{Array.from({ length: count }, (_, index) => <div className="book-card" key={index}>
|
||||
<div className="cover" />
|
||||
<span className="skeleton-line" />
|
||||
<span className="skeleton-line short" />
|
||||
</div>)}
|
||||
</div>;
|
||||
}
|
||||
Reference in new issue
Block a user