diff --git a/web/src/features/library/pages/LibraryPage.tsx b/web/src/features/library/pages/LibraryPage.tsx
index d89642b..527876d 100644
--- a/web/src/features/library/pages/LibraryPage.tsx
+++ b/web/src/features/library/pages/LibraryPage.tsx
@@ -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 &&
{listing.data.crumbs.map((crumb, index) => {index > 0 && / } navigate(crumb.path)}>{crumb.name} )}
}
- {books.isPending && }{books.error && {books.error.message} }
+ {books.isPending && <> >}{books.error && {books.error.message} }
{empty && {search ? '没有匹配的书' : '书库还没有书'}
{search ? '换个关键词,或者检查一下作者名的写法。' : '前往文件管理添加书籍,再回到这里浏览。'}
{search ? { setDraft(''); navigate(path); }}>清除搜索 : role === 'admin' ? 文件管理 : null}
}
{books.data && books.data.items.length > 0 && {books.data.items.map(book =>
{book.title || '未命名'}
{book.author || '未知作者'}
{book.shelfState === 'off' &&
void toggle(book)}>+ 加入书架 }
)}
}
{books.data && {search ? `找到 ${count} 本` : `${count} 本 · 第 ${page} / ${pages} 页`}
}
diff --git a/web/src/features/shelf/pages/ShelfPage.tsx b/web/src/features/shelf/pages/ShelfPage.tsx
index dd7cf3b..bcc6e98 100644
--- a/web/src/features/shelf/pages/ShelfPage.tsx
+++ b/web/src/features/shelf/pages/ShelfPage.tsx
@@ -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() {
{(!query.isPending && (cards.length > 0 || queryText || filtered)) && {queryText ? `找到 ${query.data?.total ?? 0} 本` : `共 ${query.data?.total ?? 0} 本`} {SHELF_SORTS.map(option => onSort(option.value)}>{option.label} )}
}
- {query.isPending && }
+ {query.isPending && <> >}
{query.error && {query.error.message} }
{query.data && cards.length === 0 && filtered && !queryText && {status ? `还没有${readingLabels[status]}的书` : '这个分组里没有书'}
{status === 'want' ? '在书籍的“⋯”菜单里可以标记想读。' : '换一个分类,或查看全部书籍。'}
查看全部书籍
}
{query.data && cards.length === 0 && !(filtered && !queryText) && {queryText ? '没有匹配的书' : '书架还没有书'}
{queryText ? '换个关键词,或者检查一下作者名的写法' : '从书库挑选喜欢的书,或通过书源搜索,加入书架后就能开始阅读。'}
{queryText ? 清除搜索 : <> 去搜书 打开书库>}
}
diff --git a/web/src/ui/book-grid-skeleton.tsx b/web/src/ui/book-grid-skeleton.tsx
new file mode 100644
index 0000000..ac722a8
--- /dev/null
+++ b/web/src/ui/book-grid-skeleton.tsx
@@ -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
+ {Array.from({ length: count }, (_, index) =>
)}
+
;
+}