diff --git a/docs/ui-review/collections-ux.md b/docs/ui-review/collections-ux.md new file mode 100644 index 0000000..48a167b --- /dev/null +++ b/docs/ui-review/collections-ux.md @@ -0,0 +1,20 @@ +# 书架与书库样式优化 + +日期:2026-09-21。 + +- 标题、辅助文字、设置入口统一;书架的“书库 / 书源”和书库的“文件管理 / 上传书籍”使用同样的图标文字入口。子目录的面包屑单列显示。 +- 搜索框统一圆角与高度;空书架隐藏排序栏,刷新移至标题栏。 +- 空状态统一为图标、标题、说明和主操作卡片。空书架可去搜书或打开书库;空书库可上传第一本书;存在未识别文件时引导查看文件管理;只读目录不显示上传操作。空库不再显示单独的“0 本”状态栏。 +- 保留搜索、排序、目录导航和有书时的封面网格。 + +验证:Web 构建及 38 项书架、书库、权限相关测试通过。生产 bundle 在 Chromium 中检查 320/390/1280px 空状态、有书、子目录、无搜索结果、只读和深色主题;验证书源、书库、文件管理、上传文件选择器、排序、搜索清除操作,以及页面和导航无横向溢出。 + +浏览器脚本使用可控 API fixture;没有上传实际文件,也未进行 Android/iOS 真机测试。 + +```powershell +npm --prefix web run build +$env:CHROME_PATH='C:\Program Files\Google\Chrome\Application\chrome.exe' +node web/tools/ui-review/collections.mjs +``` + +截图输出到本目录 `collections-*.png`,重点预览:`collections-shelf-empty-390.png`、`collections-library-empty-390.png`、`collections-shelf-books.png`、`collections-library-books.png`。 diff --git a/web/src/styles/reader.css b/web/src/styles/reader.css index 835d51d..4225702 100644 --- a/web/src/styles/reader.css +++ b/web/src/styles/reader.css @@ -3853,6 +3853,38 @@ section[aria-label="切换书源"] button { margin:.35rem .4rem .35rem 0; paddin .source-modal-content { overflow-y: auto; overscroll-behavior: contain; padding: 0 1rem 1rem; } .source-modal-actions { display: flex; flex-wrap: wrap; gap: .5rem; padding: .75rem 1rem; border-top: 1px solid var(--reader-line); flex: none; } .source-modal-actions button { min-height: 44px; } + +/* Collection pages share a quiet heading, labelled destinations and useful empty states. */ +.shelf-head { align-items: center; padding-top: calc(1.25rem + var(--safe-top)); padding-bottom: .85rem; } +.shelf-head-actions { margin-top: 0; } +.shelf-head .icon-button, .collection-heading .icon-button { width: 44px; height: 44px; border-radius: .8rem; } +.shelf-subtitle { margin-top: .45rem; line-height: 1.5; } +.collection-header { padding: calc(1.25rem + var(--safe-top)) calc(.9rem + var(--safe-right)) .75rem calc(.9rem + var(--safe-left)); flex: none; } +.collection-heading { display: flex; align-items: center; gap: .5rem; } +.collection-heading-text { flex: 1; min-width: 0; } +.collection-heading h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -.02em; line-height: 1.15; } +.collection-heading p { margin: .45rem 0 0; font-size: .78rem; line-height: 1.5; } +.collection-links { display: flex; gap: .65rem; margin-top: .85rem; } +.shelf-links { max-width: calc(var(--shelf-measure) - 1.8rem); margin: 0 auto .9rem; padding-inline: calc(.9rem + var(--safe-left)) calc(.9rem + var(--safe-right)); } +.collection-link.button { display: flex; align-items: center; gap: .6rem; flex: 1; min-width: 0; min-height: 48px; padding: .7rem .85rem; font-size: .875rem; border-radius: .8rem; background: color-mix(in srgb, var(--reader-accent) 4%, var(--reader-paper)); border-color: color-mix(in srgb, var(--reader-line) 65%, transparent); text-align: start; } +.collection-link > span { flex: 1; } +.collection-link[hidden] { display: none; } +.collection-link > .icon:first-child { color: var(--reader-accent); } +.collection-link > .icon:last-child { color: var(--reader-muted); font-size: .8rem; } +.shelf-screen .shelf-search input, .library-search input { border-radius: .85rem; min-height: 48px; } +.library-browse-screen .library-search { margin-top: .35rem; } +.library-path { margin-top: .75rem; padding: .25rem 0 0; } +.library-path .manager-crumb { min-height: 44px; } +.collection-empty.empty-state { width: calc(100% - 2rem); max-width: 30rem; margin: clamp(1.5rem, 7vh, 4rem) auto 2rem; padding: 1.75rem 1.25rem; gap: .7rem; border: 1px solid var(--reader-line); border-radius: 1.25rem; background: color-mix(in srgb, var(--reader-surface) 45%, var(--reader-paper)); } +.collection-empty .empty-glyph { box-sizing: content-box; width: 30px; height: 30px; padding: 1rem; border-radius: 1.1rem; background: color-mix(in srgb, var(--reader-accent) 9%, transparent); color: var(--reader-accent); opacity: 1; margin-bottom: .5rem; } +.collection-empty p { font-size: 1.1rem; line-height: 1.5; } +.collection-empty p.muted { max-width: 23rem; font-size: .875rem; line-height: 1.8; text-wrap: pretty; } +.collection-empty .empty-actions { margin-top: .6rem; gap: .65rem; } +.collection-empty .button { min-height: 44px; font-size: .875rem; } +.library-browse-body > .manager-status { background: transparent; border: 0; margin: .75rem auto; } +@media (min-width: 72rem) { + .collection-header, .library-browse-body { width: 100%; max-width: 68rem; margin-inline: auto; } +} @media (max-width: 600px) { .source-entry-actions { flex-direction: column; gap: 0; } .source-entry-top { align-items: center; } diff --git a/web/src/ui/library-screen.tsx b/web/src/ui/library-screen.tsx index 212e3a6..cfc258e 100644 --- a/web/src/ui/library-screen.tsx +++ b/web/src/ui/library-screen.tsx @@ -479,14 +479,18 @@ export class LibraryBrowseScreen { const listing = state.listing; return ( <> -
- this.options.onClose()} /> - {/* The way to the other half. A *place*, not a filter, so it is a labelled - control rather than a bare glyph: the reader has to know that pressing - it leaves the books and opens a file manager, because that page can - delete things. */} - this.options.onOpenFiles()} /> -
+
+
+ this.options.onClose()} /> +

书库

浏览本地书籍,发现下一本好书

+ this.patch({ settingsOpen: !state.settingsOpen })} /> +
+ + {(listing?.crumbs.length ?? 0) > 1 && <> +
{(listing?.crumbs ?? []).map((crumb, index) => ( <> {index > 0 ? / : null} @@ -502,19 +506,8 @@ export class LibraryBrowseScreen { ))}
-
+ } +
{/* @@ -558,7 +551,7 @@ export class LibraryBrowseScreen { onGo={(page) => this.goToPage(page)} /> ) : null} -
{state.status || this.summary()}
+ {(state.status || state.total > 0) &&
{state.status || this.summary()}
}
{state.dialog ? ( @@ -577,24 +570,30 @@ export class LibraryBrowseScreen { private grid(): ComponentChildren { const state = this.state; if (state.books.length === 0) { + if (state.bootstrapping) return null; + const hasFiles = (state.listing?.files ?? 0) > 0; + const writable = state.listing?.writable === true; const hasQuery = state.query.length > 0; return ( -
+
-

{hasQuery ? '没有匹配的书' : '这个文件夹里没有可阅读的书'}

+

{hasQuery ? '没有匹配的书' : hasFiles ? '这个文件夹里没有可阅读的书' : '书库还没有书'}

{hasQuery ? `「${state.query}」在这个文件夹里没有匹配` - : `${state.listing?.files ?? 0} 个文件里没有被扫描成书的,文件页能看到每一行的原因`} + : hasFiles ? '这里有 ' + state.listing!.files + ' 个文件,可在文件管理中查看识别结果。' + : writable ? '上传 EPUB、TXT 等书籍,开始建立你的书库。' : '管理员添加书籍后,就能在这里浏览并加入书架。'}

{hasQuery ? ( - - ) : ( - this.options.onOpenFiles()} /> - )} + ) : !hasFiles && writable ? ( + this.uploadInput.click()} /> + ) : hasFiles ? ( + this.options.onOpenFiles()} /> + ) : null}
); diff --git a/web/src/ui/shelf-screen.tsx b/web/src/ui/shelf-screen.tsx index 4e9d7a5..e52788e 100644 --- a/web/src/ui/shelf-screen.tsx +++ b/web/src/ui/shelf-screen.tsx @@ -836,28 +836,7 @@ export class ShelfScreen {

- {/* - The library's entry point travels with the title rather than sitting - in the toolbar: it is a *place*, not a filter, and the toolbar is where - the filters are. It carries the folder and page the reader was last in - *there*, so switching back and forth is a toggle rather than a reset. - - The two glyphs are `library` and `tune`. The row has now been through - two rounds of "这个 icon 还是很丑" (#40), and the second round is the - reason the *names* changed as well as the shapes: `gear` and - `magnifying-glass` were Font Awesome's names on our own outlines, so - anyone who knows that set opens `paths.mjs` expecting Font Awesome's - artwork and finds a different shape. The set is ours, so it is named - ours (`settings`, `search`, `menu` — see `docs/ui.md` §1.2), and the - header's two are drawn as few long strokes at 18px: `library` is three - spines on a shelf, and `tune` is one rail with two knobs. - */} - this.options.onOpenLibrary(state.libraryPath, state.libraryPage)} - /> - {this.options.onOpenSources && } + {empty && !hasQuery && void this.manualRefresh()} />} +
-
+ {(!empty || hasQuery) &&
{hasQuery || state.total > 0 ? ( {hasQuery ? `找到 ${state.total} 本` : `共 ${state.total} 本`} @@ -917,7 +900,7 @@ export class ShelfScreen { ))}
-
+
}