mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-15 20:34:23 +08:00
124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
// 书架展示与排序的纯逻辑(不依赖 React,便于单测)。
|
|
//
|
|
// 语义对齐 legado 书架:未读章数取自 getUnreadChapterNum,「最近更新」取自
|
|
// Book.latestChapterTime,排序项与 AppConfig.getBookSortByGroupId 的六个取值一一对应。
|
|
|
|
import type { ReaderBook } from '../../api/reader'
|
|
import type { ReaderShelfLayout, ReaderShelfSort } from '../../stores/readerSettings'
|
|
|
|
/** 未读章数:total_chapter_num 为 0 表示目录尚未缓存,返回 null(前端不显示徽标)。 */
|
|
export function unreadChapters(book: ReaderBook): number | null {
|
|
if (book.total_chapter_num <= 0) return null
|
|
// dur_chapter_time 为 0 表示还没开始读,否则读到 dur_chapter_index 为止。
|
|
const read = book.dur_chapter_time > 0 ? book.dur_chapter_index + 1 : 0
|
|
return Math.max(0, Math.min(book.total_chapter_num, book.total_chapter_num - read))
|
|
}
|
|
|
|
/** 书籍的「最近更新」时间(毫秒),未检测到更新时为 0。 */
|
|
export function shelfUpdateTime(book: ReaderBook): number {
|
|
return Number.isFinite(book.latest_chapter_time) ? book.latest_chapter_time : 0
|
|
}
|
|
|
|
function byTitle(a: ReaderBook, b: ReaderBook): number {
|
|
return a.name.localeCompare(b.name, 'zh-Hans-CN')
|
|
}
|
|
|
|
function byAuthor(a: ReaderBook, b: ReaderBook): number {
|
|
return (a.author || '').localeCompare(b.author || '', 'zh-Hans-CN') || byTitle(a, b)
|
|
}
|
|
|
|
/**
|
|
* 按所选方式排序书架,不修改入参。
|
|
*
|
|
* - recent:最近阅读(dur_chapter_time 倒序,没读过的按书名排后面)——legado 默认
|
|
* - update:最近更新(latest_chapter_time 倒序)
|
|
* - name / author:书名 / 作者
|
|
* - mixed:综合(阅读时间与更新时间取较新者)
|
|
* - manual:手动顺序(服务端 order 字段)
|
|
*/
|
|
export function sortShelfBooks(books: ReaderBook[], sort: ReaderShelfSort): ReaderBook[] {
|
|
const list = [...books]
|
|
switch (sort) {
|
|
case 'update':
|
|
return list.sort(
|
|
(a, b) => shelfUpdateTime(b) - shelfUpdateTime(a) || b.dur_chapter_time - a.dur_chapter_time || byTitle(a, b),
|
|
)
|
|
case 'name':
|
|
return list.sort(byTitle)
|
|
case 'author':
|
|
return list.sort(byAuthor)
|
|
case 'mixed':
|
|
return list.sort(
|
|
(a, b) =>
|
|
Math.max(shelfUpdateTime(b), b.dur_chapter_time) - Math.max(shelfUpdateTime(a), a.dur_chapter_time) ||
|
|
byTitle(a, b),
|
|
)
|
|
case 'manual':
|
|
return list.sort((a, b) => a.order - b.order || byTitle(a, b))
|
|
case 'recent':
|
|
default:
|
|
return list.sort((a, b) => b.dur_chapter_time - a.dur_chapter_time || byTitle(a, b))
|
|
}
|
|
}
|
|
|
|
/** 绝对时间:当天只显示时分,同年显示月日,跨年带年份。 */
|
|
export function formatBookTime(ms: number): string {
|
|
if (!Number.isFinite(ms) || ms <= 0) return ''
|
|
const d = new Date(ms)
|
|
const now = new Date()
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
if (d.toDateString() === now.toDateString()) return `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
if (d.getFullYear() === now.getFullYear()) return `${d.getMonth() + 1}月${d.getDate()}日`
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
}
|
|
|
|
/** 相对时间:刚刚 / N 分钟前 / N 小时前 / N 天前,超过 30 天退回日期。 */
|
|
export function formatRelativeTime(ms: number): string {
|
|
if (!Number.isFinite(ms) || ms <= 0) return ''
|
|
const diff = Date.now() - ms
|
|
if (diff < 60_000) return '刚刚'
|
|
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`
|
|
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`
|
|
if (diff < 30 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`
|
|
return formatBookTime(ms)
|
|
}
|
|
|
|
/** 「读到」文案;未开始时返回空串。 */
|
|
export function readProgressText(book: ReaderBook): string {
|
|
if (book.dur_chapter_time <= 0 || !book.dur_chapter_title) return ''
|
|
return book.dur_chapter_title
|
|
}
|
|
|
|
export const SHELF_LAYOUT_OPTIONS: { value: ReaderShelfLayout; label: string; hint: string }[] = [
|
|
{ value: 'grid', label: '网格', hint: '封面宫格,可调列数' },
|
|
{ value: 'list', label: '列表', hint: '封面 + 书名 + 读到 / 最新章节' },
|
|
{ value: 'compact', label: '紧凑列表', hint: '小封面单行,一屏放更多书' },
|
|
]
|
|
|
|
export const SHELF_SORT_OPTIONS: { value: ReaderShelfSort; label: string }[] = [
|
|
{ value: 'recent', label: '最近阅读' },
|
|
{ value: 'update', label: '最近更新' },
|
|
{ value: 'mixed', label: '综合' },
|
|
{ value: 'name', label: '按书名' },
|
|
{ value: 'author', label: '按作者' },
|
|
{ value: 'manual', label: '手动顺序' },
|
|
]
|
|
|
|
/** 网格列数选项的展示文案。 */
|
|
export function gridColumnsLabel(columns: number): string {
|
|
return columns === 0 ? '自适应' : `${columns} 列`
|
|
}
|
|
|
|
/** 书籍详情页地址(/reader/book),与搜索结果的跳转参数保持一致。 */
|
|
export function bookDetailPath(book: ReaderBook): string {
|
|
const params = new URLSearchParams({
|
|
source_url: book.origin,
|
|
book_url: book.book_url,
|
|
name: book.name,
|
|
author: book.author || '',
|
|
cover_url: book.cover_url || '',
|
|
origin_name: book.origin_name || '',
|
|
})
|
|
return `/reader/book?${params.toString()}`
|
|
}
|