152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
import { resolvePreviewUrl } from '../../config/assets';
|
|
import type { ApiCategory, ApiWorksheet } from '../../services/content-api';
|
|
|
|
export interface CategoryTab {
|
|
id: string;
|
|
name: string;
|
|
icon: string;
|
|
}
|
|
|
|
export interface CategoryItem {
|
|
id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
icon: string;
|
|
previewImg: string;
|
|
ageBand: string;
|
|
difficultyLabel: string;
|
|
path: string;
|
|
available: boolean;
|
|
likes: string;
|
|
downloads: string;
|
|
date: string;
|
|
updatedAt: string;
|
|
skeleton?: boolean;
|
|
}
|
|
|
|
export interface CategoryDataset {
|
|
sidebarTabs: CategoryTab[];
|
|
groups: Map<string, CategoryItem[]>;
|
|
}
|
|
|
|
export const BASE_TABS: CategoryTab[] = [
|
|
{ id: 'all', name: '全部', icon: '📋' },
|
|
{ id: 'hot', name: '热门推荐', icon: '🔥' },
|
|
{ id: 'top-downloads', name: '下载最多', icon: '📥' },
|
|
];
|
|
const DIFFICULTY_LABELS = ['入门', '基础', '进阶', '挑战'];
|
|
|
|
/** 比较任意长度的非负十进制计数;微信运行时无需 BigInt,也不丢失 64 位精度。 */
|
|
export function compareCountDescending(left: string, right: string): number {
|
|
const a = left.replace(/^0+(?=\d)/, '');
|
|
const b = right.replace(/^0+(?=\d)/, '');
|
|
return a.length !== b.length
|
|
? b.length - a.length
|
|
: a === b
|
|
? 0
|
|
: a < b
|
|
? 1
|
|
: -1;
|
|
}
|
|
|
|
function time(value: string): number {
|
|
const parsed = Date.parse(value);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
/** 保留原页面交互:最近更新的两项置顶,其余下载量降序,并列按稳定 ID。 */
|
|
function sortWorksheets(items: CategoryItem[]): CategoryItem[] {
|
|
const byId = (a: CategoryItem, b: CategoryItem) =>
|
|
a.id < b.id ? -1 : a.id === b.id ? 0 : 1;
|
|
const sorted = [...items].sort(
|
|
(a, b) => time(b.updatedAt) - time(a.updatedAt) || byId(a, b),
|
|
);
|
|
return [
|
|
...sorted.slice(0, 2),
|
|
...sorted
|
|
.slice(2)
|
|
.sort(
|
|
(a, b) =>
|
|
compareCountDescending(a.downloads, b.downloads) ||
|
|
byId(a, b),
|
|
),
|
|
];
|
|
}
|
|
|
|
/** 只展示本次接口返回的分类;并发下架导致目录缺失的题型直接丢弃。 */
|
|
export function buildCategoryDataset(
|
|
categories: ApiCategory[],
|
|
worksheets: ApiWorksheet[],
|
|
): CategoryDataset {
|
|
const reservedIds = new Set(BASE_TABS.map((tab) => tab.id));
|
|
const groups = new Map<string, CategoryItem[]>();
|
|
const icons = new Map<string, string>();
|
|
for (const category of categories) {
|
|
if (reservedIds.has(category.id) || groups.has(category.id))
|
|
throw new Error('分类数据异常,请稍后重试');
|
|
groups.set(category.id, []);
|
|
icons.set(category.id, category.icon || '📄');
|
|
}
|
|
for (const row of worksheets) {
|
|
const group = groups.get(row.category);
|
|
if (!group) continue;
|
|
const date = row.contentUpdatedAt || row.createdAt;
|
|
group.push({
|
|
id: row.id,
|
|
title: row.title,
|
|
subtitle: row.subtitle,
|
|
icon: icons.get(row.category) || '📄',
|
|
previewImg: resolvePreviewUrl(row.previewImg),
|
|
ageBand: `${row.ageMin}-${row.ageMax}岁`,
|
|
difficultyLabel: DIFFICULTY_LABELS[row.difficulty - 1] || '基础',
|
|
path: row.path,
|
|
available: Boolean(row.path),
|
|
likes: row.likes,
|
|
downloads: row.downloads,
|
|
date: Number.isFinite(Date.parse(date))
|
|
? new Date(date).toISOString().slice(0, 10)
|
|
: '',
|
|
updatedAt: row.updatedAt,
|
|
});
|
|
}
|
|
return {
|
|
sidebarTabs: [
|
|
...BASE_TABS,
|
|
...categories.map(({ id, name, icon }) => ({ id, name, icon })),
|
|
],
|
|
groups,
|
|
};
|
|
}
|
|
|
|
/** 两个榜单沿用旧页面含义:热门推荐按收藏数,下载最多按下载数,各取前 12。 */
|
|
export function getCategoryItems(
|
|
dataset: CategoryDataset | null,
|
|
categoryId: string,
|
|
keyword = '',
|
|
): CategoryItem[] {
|
|
if (!dataset) return [];
|
|
let items: CategoryItem[];
|
|
if (
|
|
categoryId === 'all' ||
|
|
categoryId === 'hot' ||
|
|
categoryId === 'top-downloads'
|
|
) {
|
|
const all = sortWorksheets(Array.from(dataset.groups.values()).flat());
|
|
if (categoryId === 'all') items = all;
|
|
else {
|
|
const key = categoryId === 'hot' ? 'likes' : 'downloads';
|
|
items = all
|
|
.sort((a, b) => compareCountDescending(a[key], b[key]))
|
|
.slice(0, 12);
|
|
}
|
|
} else items = sortWorksheets(dataset.groups.get(categoryId) || []);
|
|
const kw = keyword.trim().toLowerCase();
|
|
return kw
|
|
? items.filter(
|
|
(item) =>
|
|
item.title.toLowerCase().includes(kw) ||
|
|
item.subtitle.toLowerCase().includes(kw),
|
|
)
|
|
: items;
|
|
}
|