feat(lib): shelf sort/section pure helpers + formatSize extraction, table tests

This commit is contained in:
2026-09-08 17:29:07 +08:00
parent 03bc537308
commit 913f3286bb
5 changed files with 72 additions and 9 deletions
+2 -9
View File
@@ -1,5 +1,7 @@
import type { Format } from "../api/types";
export { formatSize } from "../lib/format"; // deprecated: phase-2 随阅读器清除
export const btn =
"inline-flex items-center gap-1.5 rounded-lg bg-white/[0.06] px-3 py-1.5 text-sm ring-1 ring-white/10 transition-colors hover:bg-white/[0.11] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500 active:bg-white/[0.15] disabled:cursor-not-allowed disabled:opacity-50";
export const btnPrimary =
@@ -27,12 +29,3 @@ export const FORMAT_BADGE: Record<Format, string> = {
export function formatBadge(fmt: Format, extra = ""): string {
return `rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ring-1 ${FORMAT_BADGE[fmt]} ${extra}`;
}
export function formatSize(bytes: number): string {
if (bytes <= 0) return "";
const units = ["B", "KB", "MB", "GB"];
let i = 0;
let n = bytes;
for (; n >= 1024 && i < units.length - 1; i++) n /= 1024;
return `${n >= 10 || i === 0 ? Math.round(n) : n.toFixed(1)} ${units[i]}`;
}
+8
View File
@@ -0,0 +1,8 @@
export function formatSize(bytes: number): string {
if (bytes <= 0) return "";
const units = ["B", "KB", "MB", "GB"];
let i = 0;
let n = bytes;
for (; n >= 1024 && i < units.length - 1; i++) n /= 1024;
return `${n >= 10 || i === 0 ? Math.round(n) : n.toFixed(1)} ${units[i]}`;
}
+19
View File
@@ -0,0 +1,19 @@
import type { Book } from "../api/types";
import { groupByDir } from "./group";
export type SortKey = "added" | "title" | "progress";
export const SORT_LABEL: Record<SortKey, string> = { added: "最近入库", title: "标题", progress: "阅读进度" };
export type GroupMode = "flat" | "dir";
export function sortBooks(books: Book[], key: SortKey): Book[] {
const a = [...books];
if (key === "title") return a.sort((x, y) => x.title.localeCompare(y.title, "zh"));
if (key === "progress") return a.sort((x, y) => y.percent - x.percent || (x.added_at < y.added_at ? 1 : x.added_at > y.added_at ? -1 : 0));
return a.sort((x, y) => (x.added_at < y.added_at ? 1 : x.added_at > y.added_at ? -1 : 0));
}
/** 页面分组视图:flat 一段无标题;dir 沿用「根目录置顶、目录名 locale 排序」。 */
export function sections(books: Book[], key: SortKey, mode: GroupMode): { label: string; books: Book[] }[] {
if (mode === "flat") return [{ label: "", books: sortBooks(books, key) }];
return groupByDir(sortBooks(books, key)).map((g) => ({ label: g.dir, books: g.books }));
}
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { formatSize } from "../src/lib/format";
describe("formatSize", () => {
it("renders human sizes across units", () => {
expect(formatSize(0)).toBe("");
expect(formatSize(512)).toBe("512 B");
expect(formatSize(2048)).toBe("2.0 KB");
expect(formatSize(15 * 1024 * 1024)).toBe("15 MB");
expect(formatSize(3.2 * 1024 ** 3)).toBe("3.2 GB");
});
it("returns empty for non-positive and keeps one decimal below 10", () => {
expect(formatSize(-1)).toBe("");
expect(formatSize(1024)).toBe("1.0 KB");
expect(formatSize(10 * 1024)).toBe("10 KB");
});
});
+25
View File
@@ -0,0 +1,25 @@
import { expect, it } from "vitest";
import type { Book } from "../src/api/types";
import { sections, sortBooks } from "../src/lib/shelf";
const b = (over: Partial<Book>): Book =>
({ id: 1, path: "p", title: "t", format: "cbz", size: 1, pages: 1, percent: 0, added_at: "2026-01-01T00:00:00Z", library_id: 1, state: "ready", cover_url: "", page_url_fmt: "", pages_url: "", error: "", ...over }) as Book;
it("sortBooks: added 新→旧 / title zh 本地化 / progress 高→低且并列按 added", () => {
const old = b({ id: 1, added_at: "2026-01-01T00:00:00Z", title: "b" });
const nw = b({ id: 2, added_at: "2026-02-01T00:00:00Z", title: "a" });
expect(sortBooks([old, nw], "added").map((x) => x.id)).toEqual([2, 1]);
expect(sortBooks([old, nw], "title").map((x) => x.id)).toEqual([2, 1]);
expect(sortBooks([b({ id: 3, percent: 0.2 }), b({ id: 4, percent: 0.9 })], "progress").map((x) => x.id)).toEqual([4, 3]);
const tie = [b({ id: 5, percent: 0.5, added_at: "2026-01-01T00:00:00Z" }), b({ id: 6, percent: 0.5, added_at: "2026-03-01T00:00:00Z" })];
expect(sortBooks(tie, "progress").map((x) => x.id)).toEqual([6, 5]);
});
it("sections: flat 单段无标题;dir 按目录分组且组内排序、根目录置顶", () => {
const books = [b({ id: 1, path: "x/a.cbz" }), b({ id: 2, path: "b.cbz", title: "B" }), b({ id: 3, path: "x/c.cbz", title: "A" })];
// 容器 Intl zh 实测序 A < B < t (localeCompare("zh")),故 title 升序为 id3,id2,id1
expect(sections(books, "title", "flat")).toEqual([{ label: "", books: [books[2], books[1], books[0]] }]);
const g = sections(books, "title", "dir");
expect(g.map((s) => s.label)).toEqual(["", "x"]);
expect(g[1].books.map((x) => x.title)).toEqual(["A", "t"]); // x 组内按 title 排序
});