295 lines
8.8 KiB
TypeScript
295 lines
8.8 KiB
TypeScript
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
|
|
import { takePendingCategoryTabId } from '../../utils/index';
|
|
import { CATEGORY_DATA, type CategoryItem } from './category.data';
|
|
|
|
type CategoryTab = {
|
|
id: string;
|
|
name: string;
|
|
icon: string;
|
|
};
|
|
|
|
type CategoryDataset = {
|
|
searchPlaceholder: string;
|
|
categories: Array<{
|
|
id: string;
|
|
name: string;
|
|
icon: string;
|
|
items: CategoryItem[];
|
|
}>;
|
|
};
|
|
|
|
const DIFFICULTY_LABELS: Record<number, string> = {
|
|
1: '入门',
|
|
2: '基础',
|
|
3: '进阶',
|
|
4: '挑战',
|
|
};
|
|
|
|
const DYNAMIC_TABS: CategoryTab[] = [
|
|
{ id: 'hot', name: '热门推荐', icon: '🔥' },
|
|
{ id: 'top-downloads', name: '下载最多', icon: '📥' },
|
|
];
|
|
|
|
const SIDEBAR_TABS: CategoryTab[] = [
|
|
{ id: 'all', name: '全部', icon: '📋' },
|
|
...DYNAMIC_TABS,
|
|
...CATEGORY_LIST_WITH_ALL.filter((c) => c.id !== 'all').map(
|
|
({ id, name, icon }) => ({ id, name, icon }),
|
|
),
|
|
];
|
|
|
|
const TAB_BAR_PATHS = new Set([
|
|
'/pages/home/home',
|
|
'/pages/category/category',
|
|
'/pages/age/age',
|
|
'/pages/favorites/favorites',
|
|
'/pages/profile/profile',
|
|
]);
|
|
|
|
// Page-level mutable state (shared across methods)
|
|
let _categoryData: CategoryDataset | null = null;
|
|
|
|
/** 将云端 worksheet 原始数据转换为 CategoryItem */
|
|
function toDisplayItem(raw: Record<string, any>): CategoryItem {
|
|
const difficulty = (Number(raw.difficulty) || 2) as 1 | 2 | 3 | 4;
|
|
const ageMin = Number(raw.ageMin) || 0;
|
|
const ageMax = Number(raw.ageMax) || 0;
|
|
return {
|
|
id: String(raw._id || ''),
|
|
title: String(raw.title || ''),
|
|
subtitle: String(raw.subtitle || ''),
|
|
previewImg: String(raw.previewImg || ''),
|
|
ageBand: `${ageMin}-${ageMax}岁`,
|
|
ageMin,
|
|
ageMax,
|
|
difficulty,
|
|
difficultyLabel: DIFFICULTY_LABELS[difficulty] || '基础',
|
|
path: String(raw.path || ''),
|
|
available: true,
|
|
likes: Number(raw.likes) || 0,
|
|
downloads: Number(raw.downloads) || 0,
|
|
date: raw.updatedAt
|
|
? new Date(raw.updatedAt).toISOString().slice(0, 10)
|
|
: new Date().toISOString().slice(0, 10),
|
|
updatedAt: raw.updatedAt ? String(raw.updatedAt) : '',
|
|
};
|
|
}
|
|
|
|
/** 排序:updatedAt 最新的 2 个置顶,其余按 downloads 降序 */
|
|
function sortWorksheets(items: CategoryItem[]): CategoryItem[] {
|
|
const byTime = [...items].sort(
|
|
(a, b) =>
|
|
new Date(b.updatedAt || '').getTime() -
|
|
new Date(a.updatedAt || '').getTime(),
|
|
);
|
|
const recent = byTime.slice(0, 2);
|
|
const recentIds = new Set(recent.map((w) => w.id));
|
|
const rest = byTime
|
|
.filter((w) => !recentIds.has(w.id))
|
|
.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
return [...recent, ...rest];
|
|
}
|
|
|
|
/** 将云端 worksheet 数组按分类分组,构建 CategoryDataset */
|
|
function buildCategoryDataFromCloud(
|
|
rawList: Record<string, any>[],
|
|
): CategoryDataset {
|
|
const itemsByCategory: Record<string, CategoryItem[]> = {};
|
|
for (const raw of rawList) {
|
|
const item = toDisplayItem(raw);
|
|
const cat = String(raw.category || '');
|
|
if (!itemsByCategory[cat]) itemsByCategory[cat] = [];
|
|
itemsByCategory[cat].push(item);
|
|
}
|
|
|
|
const categories = CATEGORY_LIST_WITH_ALL.filter((c) => c.id !== 'all').map(
|
|
(cat) => ({
|
|
id: cat.id,
|
|
name: cat.name,
|
|
icon: cat.icon,
|
|
items: sortWorksheets(itemsByCategory[cat.id] || []),
|
|
}),
|
|
);
|
|
|
|
return { searchPlaceholder: '搜索练习纸...', categories };
|
|
}
|
|
|
|
function buildAllItems(): CategoryItem[] {
|
|
if (!_categoryData) return [];
|
|
const items: CategoryItem[] = [];
|
|
for (const group of _categoryData.categories) {
|
|
for (const item of group.items) {
|
|
items.push({ ...item });
|
|
}
|
|
}
|
|
return sortWorksheets(items);
|
|
}
|
|
|
|
const DYNAMIC_LIMIT = 12;
|
|
|
|
function buildHotItems(): CategoryItem[] {
|
|
const all = buildAllItems();
|
|
return [...all].sort((a, b) => (b.likes || 0) - (a.likes || 0)).slice(0, DYNAMIC_LIMIT);
|
|
}
|
|
|
|
function buildTopDownloadsItems(): CategoryItem[] {
|
|
const all = buildAllItems();
|
|
return [...all].sort((a, b) => (b.downloads || 0) - (a.downloads || 0)).slice(0, DYNAMIC_LIMIT);
|
|
}
|
|
|
|
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
|
if (!_categoryData) return [];
|
|
if (categoryId === 'all') return buildAllItems();
|
|
if (categoryId === 'hot') return buildHotItems();
|
|
if (categoryId === 'top-downloads') return buildTopDownloadsItems();
|
|
const group = _categoryData.categories.find((c) => c.id === categoryId);
|
|
if (!group) return [];
|
|
return group.items.map((item) => ({ ...item }));
|
|
}
|
|
|
|
function filterByKeyword(
|
|
items: CategoryItem[],
|
|
keyword: string,
|
|
): CategoryItem[] {
|
|
if (!keyword) return items;
|
|
const kw = keyword.toLowerCase();
|
|
return items.filter(
|
|
(item) =>
|
|
item.title.toLowerCase().includes(kw) ||
|
|
item.subtitle.toLowerCase().includes(kw),
|
|
);
|
|
}
|
|
|
|
// Skeleton placeholder items for loading state
|
|
const SKELETON_ITEMS = Array.from({ length: 6 }, (_, i) => ({
|
|
id: `skeleton-${i}`,
|
|
title: '\u00A0',
|
|
subtitle: '\u00A0',
|
|
skeleton: true,
|
|
}));
|
|
|
|
Page({
|
|
data: {
|
|
searchPlaceholder: '搜索练习纸...',
|
|
searchKeyword: '',
|
|
sidebarTabs: SIDEBAR_TABS,
|
|
activeCategoryId: 'all',
|
|
displayItems: SKELETON_ITEMS as any[],
|
|
scrollIntoViewId: '',
|
|
loading: true,
|
|
},
|
|
|
|
_defaultCategoryId: 'all',
|
|
|
|
onLoad(options: Record<string, string>) {
|
|
if (options.id && options.id !== 'all') {
|
|
this._defaultCategoryId = options.id;
|
|
this.setData({ activeCategoryId: options.id });
|
|
}
|
|
},
|
|
|
|
onShow() {
|
|
_categoryData = null;
|
|
this.setData({ displayItems: SKELETON_ITEMS, loading: true });
|
|
void this.loadWorksheets(takePendingCategoryTabId() ?? undefined);
|
|
},
|
|
|
|
async loadWorksheets(overrideCategoryId?: string | null) {
|
|
try {
|
|
const res = await wx.cloud.callFunction({
|
|
name: 'worksheetsQuery',
|
|
data: { status: 'active' },
|
|
});
|
|
const result = (res?.result as any) || {};
|
|
if (!result.success) throw new Error(result.message || '查询失败');
|
|
|
|
_categoryData = buildCategoryDataFromCloud(result.data || []);
|
|
} catch {
|
|
_categoryData = CATEGORY_DATA;
|
|
}
|
|
|
|
const activeId =
|
|
overrideCategoryId != null &&
|
|
String(overrideCategoryId).trim() !== ''
|
|
? String(overrideCategoryId).trim()
|
|
: this.data.activeCategoryId || this._defaultCategoryId;
|
|
const items = getItemsByCategory(activeId);
|
|
const filtered = filterByKeyword(items, this.data.searchKeyword);
|
|
|
|
this.setData({
|
|
loading: false,
|
|
searchPlaceholder: _categoryData.searchPlaceholder,
|
|
activeCategoryId: activeId,
|
|
displayItems: filtered,
|
|
});
|
|
},
|
|
|
|
onTapCategory(e: WechatMiniprogram.TouchEvent) {
|
|
const id = e.currentTarget.dataset.id as string;
|
|
if (!id || id === this.data.activeCategoryId) return;
|
|
|
|
const items = getItemsByCategory(id);
|
|
const filtered = filterByKeyword(items, this.data.searchKeyword);
|
|
|
|
this.setData({
|
|
activeCategoryId: id,
|
|
displayItems: filtered,
|
|
scrollIntoViewId: '',
|
|
});
|
|
},
|
|
|
|
onSearchInput(e: WechatMiniprogram.Input) {
|
|
const keyword = (e.detail.value ?? '').trim();
|
|
const items = getItemsByCategory(this.data.activeCategoryId);
|
|
const filtered = filterByKeyword(items, keyword);
|
|
|
|
this.setData({
|
|
searchKeyword: keyword,
|
|
displayItems: filtered,
|
|
});
|
|
},
|
|
|
|
onClearSearch() {
|
|
const items = getItemsByCategory(this.data.activeCategoryId);
|
|
this.setData({
|
|
searchKeyword: '',
|
|
displayItems: items,
|
|
});
|
|
},
|
|
|
|
onTapItem(e: WechatMiniprogram.TouchEvent) {
|
|
const path = e.currentTarget.dataset.path as string;
|
|
const title = e.currentTarget.dataset.title as string;
|
|
const available = e.currentTarget.dataset.available;
|
|
|
|
if (!available || !path) {
|
|
wx.showToast({
|
|
title: title ? `${title} 即将上线` : '即将上线',
|
|
icon: 'none',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (TAB_BAR_PATHS.has(path)) {
|
|
wx.switchTab({ url: path });
|
|
return;
|
|
}
|
|
|
|
wx.navigateTo({ url: path });
|
|
},
|
|
|
|
onShareAppMessage() {
|
|
return {
|
|
title: '涂鸦丫 - 全部练习分类',
|
|
path: '/pages/category/category',
|
|
};
|
|
},
|
|
|
|
onShareTimeline() {
|
|
return {
|
|
title: '涂鸦丫 - 全部练习分类',
|
|
query: '',
|
|
};
|
|
},
|
|
});
|