feat: 书库支持多选批量整理与移除,修正侧栏选中亮条

工具栏新增「选择」模式,卡片显示复选框,工具条提供全选、批量整理与
批量移除。多选时隐藏单卡操作并停用封面阅读入口,避免勾选途中误开
阅读器。全选只覆盖当前筛选结果,切换书架、标签或搜索后会剔除已不
可见的选中项,不对看不见的书执行批量操作。

批量标签用三态复选框:indeterminate 表示部分书持有,跳过即保持原样,
只有明确勾上或取消才统一应用。沿用单本的覆盖语义会静默抹掉其它书
已有的标签。所选书籍分属不同书架时,下拉默认「保持不变」。

侧栏行内的重命名与删除按钮改为绝对定位。此前它们虽然 opacity 为 0,
仍占据 flex 空间,使书架与标签的选中亮条比「全部书籍」窄 47px。书架
列表原为普通 block 无行间距,现与标签列表共用 .library-filter-list,
两处间距一致。
This commit is contained in:
lofyer
2026-08-03 17:18:33 +08:00
parent 381c07733a
commit 522b0f74a5
5 changed files with 488 additions and 17 deletions
+52
View File
@@ -501,6 +501,58 @@ test('书架操作对键盘焦点可见', () => {
assert.doesNotMatch(css, /\.library-shelf-actions\s*\{\s*display:\s*none/);
});
test('侧栏行内操作不占据布局,选中高亮与静态筛选项等宽', () => {
const css = fs.readFileSync(path.join(__dirname, '..', 'ui', 'style.css'), 'utf8');
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
// 留在文档流里会占掉约 47px,使书架/标签的选中亮条比「全部书籍」窄一截
const actionsRule = css.match(
/\.library-shelf-actions,\s*\n\.library-tag-actions\s*\{([^}]*)\}/
);
assert.ok(actionsRule, '书架与标签操作区应共用同一条规则');
assert.match(actionsRule[1], /position:\s*absolute/);
assert.doesNotMatch(css, /\.library-shelf-actions\s*\{\s*display:\s*flex;\s*flex:\s*none/);
assert.match(css, /\.library-shelf-row \.library-filter,\s*\n\.library-tag-row \.library-filter\s*\{[^}]*padding-right:\s*48px/);
// 书架与标签共用同一个列表容器类,行间距不会一边有一边没有
assert.match(css, /\.library-filter-list\s*\{[^}]*gap:\s*1px/);
assert.match(html, /id="libraryShelfList" class="library-filter-list"/);
assert.match(html, /id="libraryTagList" class="library-filter-list"/);
assert.doesNotMatch(css, /\.library-tag-list\s*\{/);
});
test('书库多选提供全选、批量整理与批量移除', () => {
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
const library = fs.readFileSync(libFile, 'utf8');
assert.match(html, /id="librarySelectModeBtn"[^>]*aria-pressed="false"/);
assert.match(html, /id="librarySelectAll"/);
assert.match(html, /id="libraryBulkOrganizeBtn"[^>]*disabled/);
assert.match(html, /id="libraryBulkRemoveBtn"[^>]*disabled/);
// 全选只覆盖当前筛选结果,且被筛掉的条目要从选中集合里剔除,
// 否则会对用户看不见的书执行批量操作
assert.match(library, /visibleIds = items\.map\(\(item\) => String\(item\.id\)\)/);
assert.match(library, /if \(!visible\.has\(id\)\) selectedIds\.delete\(id\)/);
assert.match(library, /visibleIds\.forEach\(\(id\) => selectedIds\.add\(id\)\)/);
// 多选时封面不能再触发阅读,否则勾选途中会误开阅读器
assert.match(library, /const coverActs = readable && !selectMode/);
assert.match(library, /\$\{coverActs \? 'data-act="read"/);
// 批量标签是增量语义:indeterminate 表示部分持有,跳过即保持原样
assert.match(library, /if \(box\.indeterminate\) return;/);
assert.match(library, /else if \(box\.dataset\.state !== 'none'\) strip\.push/);
assert.match(library, /box\.indeterminate = next === 'some'/);
assert.doesNotMatch(library, /#libraryBulkTags input\[type="checkbox"\]:checked/);
// 书架不一致时默认保持不变,不能把所选书籍统一挪走
assert.match(library, /if \(shelfValue !== '__keep__'\) patch\.shelfId/);
assert.match(library, /value="__keep__" selected>保持不变/);
// 批量移除沿用单本移除的两个可选项
assert.match(library, /id="bulkDelFiles"/);
assert.match(library, /id="bulkDelReadingData"/);
assert.match(library, /window\.api\.library\.remove\(item\.id, choice\)/);
});
test('书库长标题保持单行省略并提供完整悬浮提示', () => {
const library = fs.readFileSync(libFile, 'utf8');
const css = fs.readFileSync(path.join(__dirname, '..', 'ui', 'style.css'), 'utf8');