feat: 首页、分类页加载数据逻辑重构
@@ -45,7 +45,11 @@
|
||||
"preloadRule": {
|
||||
"pages/home/home": {
|
||||
"network": "all",
|
||||
"packages": ["mathPages", "focusPages", "englishPages"]
|
||||
"packages": ["mathPages", "focusPages"]
|
||||
},
|
||||
"pages/category/category": {
|
||||
"network": "all",
|
||||
"packages": ["focusPages", "englishPages"]
|
||||
},
|
||||
"pages/profile/profile": {
|
||||
"network": "all",
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { defaultPrintConfig } from './config/config';
|
||||
import { generateUUID, getAppUUID, setAppUUID } from './utils/uuid';
|
||||
|
||||
const STORAGE_KEY_PAGE_CONFIG = 'pageConfig';
|
||||
|
||||
// Pending resolvers for getPageConfig() callers waiting on config
|
||||
let _configResolvers: Array<(config: PageConfigData | null) => void> = [];
|
||||
|
||||
// app.ts
|
||||
App<IAppOption>({
|
||||
globalData: {
|
||||
env: 'release',
|
||||
uuid: '',
|
||||
printConfig: defaultPrintConfig,
|
||||
pageConfig: undefined,
|
||||
pageConfigReady: false,
|
||||
},
|
||||
onLaunch() {
|
||||
wx.cloud.init({
|
||||
@@ -16,15 +23,8 @@ App<IAppOption>({
|
||||
|
||||
const accountInfo = wx.getAccountInfoSync();
|
||||
const env = accountInfo.miniProgram.envVersion || 'release';
|
||||
/**
|
||||
* 设置环境变量
|
||||
* 开发环境:develop
|
||||
* 体验环境:trial
|
||||
* 正式环境:release
|
||||
*/
|
||||
this.globalData.env = env;
|
||||
|
||||
// 从 localStorage 读取 UUID,如果没有则生成并存储
|
||||
let uuid = getAppUUID();
|
||||
if (!uuid) {
|
||||
uuid = generateUUID();
|
||||
@@ -38,15 +38,13 @@ App<IAppOption>({
|
||||
wx.getStorageSync('printConfig') || defaultPrintConfig;
|
||||
this.globalData.printConfig = printConfig;
|
||||
}
|
||||
|
||||
void this.loadPageConfig();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 小程序显示时的生命周期
|
||||
},
|
||||
onShow() {},
|
||||
|
||||
onHide() {
|
||||
// 小程序隐藏时的生命周期
|
||||
},
|
||||
onHide() {},
|
||||
|
||||
getPrintConfig(): PrintConfig {
|
||||
return this.globalData.printConfig || defaultPrintConfig;
|
||||
@@ -56,4 +54,80 @@ App<IAppOption>({
|
||||
this.globalData.printConfig = config;
|
||||
wx.setStorageSync('printConfig', config);
|
||||
},
|
||||
|
||||
async loadPageConfig() {
|
||||
let config: PageConfigData | null = null;
|
||||
|
||||
// L1: 数据预拉取
|
||||
try {
|
||||
const res = await new Promise<{ fetchedData: string }>(
|
||||
(resolve, reject) => {
|
||||
wx.getBackgroundFetchData({
|
||||
fetchType: 'pre',
|
||||
success: resolve as any,
|
||||
fail: reject,
|
||||
});
|
||||
},
|
||||
);
|
||||
if (res.fetchedData) {
|
||||
const parsed = JSON.parse(res.fetchedData);
|
||||
config = parsed?.data || parsed;
|
||||
}
|
||||
} catch {
|
||||
// 预拉取失败,继续降级
|
||||
}
|
||||
|
||||
// L2: storage 缓存
|
||||
if (!config) {
|
||||
try {
|
||||
const cached = wx.getStorageSync(STORAGE_KEY_PAGE_CONFIG);
|
||||
if (cached && typeof cached === 'object') {
|
||||
config = cached;
|
||||
}
|
||||
} catch {
|
||||
// storage 读取失败,继续降级
|
||||
}
|
||||
}
|
||||
|
||||
// L3: 云函数兜底
|
||||
if (!config) {
|
||||
try {
|
||||
const res = (await wx.cloud.callFunction({
|
||||
name: 'pageConfigFetch',
|
||||
})) as { result?: { success?: boolean; data?: any } };
|
||||
if (res.result?.success && res.result.data) {
|
||||
config = res.result.data;
|
||||
}
|
||||
} catch {
|
||||
// 云函数也失败,config 保持 null
|
||||
}
|
||||
}
|
||||
|
||||
// 写入 globalData
|
||||
if (config) {
|
||||
this.globalData.pageConfig = config;
|
||||
// 成功获取后写入 storage 缓存
|
||||
try {
|
||||
wx.setStorageSync(STORAGE_KEY_PAGE_CONFIG, config);
|
||||
} catch {
|
||||
// storage 写入失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
// 标记就绪,通知等待中的页面
|
||||
this.globalData.pageConfigReady = true;
|
||||
for (const resolve of _configResolvers) {
|
||||
resolve(config);
|
||||
}
|
||||
_configResolvers = [];
|
||||
},
|
||||
|
||||
getPageConfig(): Promise<PageConfigData | null> {
|
||||
if (this.globalData.pageConfigReady) {
|
||||
return Promise.resolve(this.globalData.pageConfig || null);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
_configResolvers.push(resolve);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -46,7 +46,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'color-pattern',
|
||||
title: '颜色找规律',
|
||||
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||
subtitle: '观察颜色规律,涂色',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 2,
|
||||
@@ -66,7 +66,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'line-recognition',
|
||||
title: '线条识别',
|
||||
subtitle: '认识不同线条,画出颜色对应的线条',
|
||||
subtitle: '认识不同线条,画对应线条',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -86,7 +86,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'code-connect',
|
||||
title: '译码连线',
|
||||
subtitle: '按数字顺序将数字对应颜色连线',
|
||||
subtitle: '按数字顺序连线',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 3,
|
||||
|
||||
@@ -16,7 +16,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'number-find',
|
||||
title: '找数字,涂一涂',
|
||||
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||
subtitle: '找出目标数字并涂色',
|
||||
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -26,7 +27,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'number-write',
|
||||
title: '看数字,写一写',
|
||||
subtitle: '按笔画顺序练习书写数字',
|
||||
subtitle: '按笔画顺序书写数字',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -36,7 +37,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'number-coloring',
|
||||
title: '按数字,涂颜色',
|
||||
subtitle: '按指定数字给对应圆圈涂色',
|
||||
subtitle: '按指定数字涂色',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -46,7 +47,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'counting-matching',
|
||||
title: '数一数,连一连',
|
||||
subtitle: '连线配对数字和对应数量图形',
|
||||
subtitle: '连线配对数字和数量',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -56,7 +57,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'number-object-match',
|
||||
title: '数物连线',
|
||||
subtitle: '连线相同数量的物品和数字',
|
||||
subtitle: '连线数量相同的物体和数字',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -66,7 +67,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'number-object-fill',
|
||||
title: '数物填写',
|
||||
subtitle: '数物品数量,填写对应数字',
|
||||
subtitle: '数出数量,填写对应数字',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -76,7 +77,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'counting-select',
|
||||
title: '数一数,选一选',
|
||||
subtitle: '数出物品数量,圈出正确答案',
|
||||
subtitle: '数出数量,圈出正确答案',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -116,7 +117,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'missing-number',
|
||||
title: '填上缺少的数字',
|
||||
subtitle: '在数列中找出并填写缺失数字',
|
||||
subtitle: '找出并填写缺失数字',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 2,
|
||||
@@ -146,7 +147,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'one-digit-addition',
|
||||
title: '一位数加法',
|
||||
subtitle: '通过圆点学习一位数加法运算',
|
||||
subtitle: '通过圆点学习一位数加法',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 2,
|
||||
@@ -156,7 +157,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'addition-5',
|
||||
title: '5以内加法',
|
||||
subtitle: '图形化展示 5 以内加法',
|
||||
subtitle: '图形化展示5以内加法',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
difficulty: 1,
|
||||
@@ -166,7 +167,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'addition-10',
|
||||
title: '10以内加法',
|
||||
subtitle: '图形化展示 10 以内加法',
|
||||
subtitle: '图形化展示10以内加法',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 2,
|
||||
@@ -176,7 +177,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'subtraction-10',
|
||||
title: '10以内减法',
|
||||
subtitle: '图形化展示 10 以内减法',
|
||||
subtitle: '图形化展示10以内减法',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 2,
|
||||
@@ -196,7 +197,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'make-ten',
|
||||
title: '凑十法练习',
|
||||
subtitle: '20 以内进位加法',
|
||||
subtitle: '20以内进位加法',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 3,
|
||||
@@ -206,7 +207,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'break-ten',
|
||||
title: '破十法练习',
|
||||
subtitle: '20 以内退位减法',
|
||||
subtitle: '20以内退位减法',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 3,
|
||||
@@ -216,7 +217,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'flat-ten',
|
||||
title: '平十法练习',
|
||||
subtitle: '20 以内退位减法',
|
||||
subtitle: '20以内退位减法',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 3,
|
||||
@@ -246,7 +247,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'practice-subtraction',
|
||||
title: '减法运算',
|
||||
subtitle: '10/20/50/100 以内减法',
|
||||
subtitle: '10/20/50/100以内减法',
|
||||
ageMin: 5,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
@@ -256,7 +257,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
{
|
||||
id: 'practice-mixed',
|
||||
title: '混合运算',
|
||||
subtitle: '10/20/50/100 以内加减法混合',
|
||||
subtitle: '10/20/50/100以内加减法混合',
|
||||
ageMin: 5,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
|
||||
@@ -77,6 +77,7 @@ function item(input: ItemInput): CategoryItem {
|
||||
};
|
||||
}
|
||||
|
||||
/** 目前只作为兜底数据 */
|
||||
const CATEGORY_ITEMS_BY_ID: Record<string, CategoryItem[]> = {
|
||||
math: [
|
||||
item({
|
||||
|
||||
@@ -142,8 +142,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid #bbb;
|
||||
background: #f0f0f0;
|
||||
border: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
background: #f5edd8;
|
||||
}
|
||||
|
||||
.cat-card__thumb-img {
|
||||
@@ -261,4 +261,46 @@
|
||||
.cat-empty__text {
|
||||
font-size: 28rpx;
|
||||
color: @text-gray;
|
||||
}
|
||||
|
||||
// ── Skeleton ──
|
||||
@keyframes skeleton-pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.cat-card--skeleton {
|
||||
pointer-events: none;
|
||||
|
||||
.cat-card__thumb {
|
||||
background: #e8e2d5;
|
||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cat-card__title,
|
||||
.cat-card__subtitle {
|
||||
background: #e8e2d5;
|
||||
border-radius: 8rpx;
|
||||
color: transparent;
|
||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cat-card__title {
|
||||
width: 60%;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.cat-card__subtitle {
|
||||
width: 80%;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,16 @@ type CategoryTab = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
type CategoryDataset = {
|
||||
searchPlaceholder: string;
|
||||
categories: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
items: CategoryItem[];
|
||||
}>;
|
||||
};
|
||||
|
||||
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
||||
({ id, name, icon }) => ({
|
||||
id,
|
||||
@@ -15,25 +25,34 @@ const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
||||
}),
|
||||
);
|
||||
|
||||
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, set once on load)
|
||||
let _categoryData: CategoryDataset | null = null;
|
||||
|
||||
function buildAllItems(): CategoryItem[] {
|
||||
if (!_categoryData) return [];
|
||||
const items: CategoryItem[] = [];
|
||||
for (const group of CATEGORY_DATA.categories) {
|
||||
for (const group of _categoryData.categories) {
|
||||
for (const item of group.items) {
|
||||
items.push({
|
||||
...item,
|
||||
});
|
||||
items.push({ ...item });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
||||
if (!_categoryData) return [];
|
||||
if (categoryId === 'all') return buildAllItems();
|
||||
const group = CATEGORY_DATA.categories.find((c) => c.id === categoryId);
|
||||
const group = _categoryData.categories.find((c) => c.id === categoryId);
|
||||
if (!group) return [];
|
||||
return group.items.map((item) => ({
|
||||
...item,
|
||||
}));
|
||||
return group.items.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
function filterByKeyword(
|
||||
@@ -49,33 +68,64 @@ function filterByKeyword(
|
||||
);
|
||||
}
|
||||
|
||||
const TAB_BAR_PATHS = new Set([
|
||||
'/pages/home/home',
|
||||
'/pages/category/category',
|
||||
'/pages/age/age',
|
||||
'/pages/favorites/favorites',
|
||||
'/pages/profile/profile',
|
||||
]);
|
||||
// Skeleton placeholder items for loading state
|
||||
const SKELETON_ITEMS = Array.from({ length: 4 }, (_, i) => ({
|
||||
id: `skeleton-${i}`,
|
||||
title: '\u00A0',
|
||||
subtitle: '\u00A0',
|
||||
skeleton: true,
|
||||
}));
|
||||
|
||||
Page({
|
||||
data: {
|
||||
searchPlaceholder: CATEGORY_DATA.searchPlaceholder,
|
||||
searchPlaceholder: '搜索练习纸...',
|
||||
searchKeyword: '',
|
||||
sidebarTabs: SIDEBAR_TABS,
|
||||
activeCategoryId: 'all',
|
||||
displayItems: buildAllItems() as CategoryItem[],
|
||||
displayItems: SKELETON_ITEMS as any[],
|
||||
scrollIntoViewId: '',
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad(options: Record<string, string>) {
|
||||
const id = options.id;
|
||||
if (id && id !== 'all') {
|
||||
const items = getItemsByCategory(id);
|
||||
this.setData({
|
||||
activeCategoryId: id,
|
||||
displayItems: items,
|
||||
});
|
||||
void this.initData(options);
|
||||
},
|
||||
|
||||
async initData(options: Record<string, string>) {
|
||||
wx.showLoading({ title: '加载中', mask: false });
|
||||
|
||||
const app = getApp<IAppOption>();
|
||||
const config = await app.getPageConfig();
|
||||
|
||||
// Use cloud config if available, otherwise fallback to local static data
|
||||
if (config?.category?.categories?.length) {
|
||||
_categoryData = {
|
||||
searchPlaceholder:
|
||||
config.category.searchPlaceholder || '搜索练习纸...',
|
||||
categories: config.category.categories.map((cat) => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
icon: cat.icon,
|
||||
items: (cat.items || []).map(
|
||||
(raw) => raw as unknown as CategoryItem,
|
||||
),
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
_categoryData = CATEGORY_DATA;
|
||||
}
|
||||
|
||||
const activeId = options.id && options.id !== 'all' ? options.id : 'all';
|
||||
const items = getItemsByCategory(activeId);
|
||||
|
||||
this.setData({
|
||||
loading: false,
|
||||
searchPlaceholder: _categoryData.searchPlaceholder,
|
||||
activeCategoryId: activeId,
|
||||
displayItems: items,
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
},
|
||||
|
||||
onTapCategory(e: WechatMiniprogram.TouchEvent) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<nav-bar title="分类" />
|
||||
|
||||
<view class="cat-body">
|
||||
<view class="cat-search-wrap">
|
||||
<!-- <view class="cat-search-wrap">
|
||||
<view class="cat-search">
|
||||
<van-icon
|
||||
name="search"
|
||||
@@ -23,7 +23,7 @@
|
||||
color="rgba(50, 46, 37, 0.35)"
|
||||
bindtap="onClearSearch" />
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<view class="cat-main">
|
||||
<scroll-view
|
||||
@@ -52,18 +52,20 @@
|
||||
<view
|
||||
wx:for="{{displayItems}}"
|
||||
wx:key="id"
|
||||
class="cat-card"
|
||||
class="cat-card {{item.skeleton ? 'cat-card--skeleton' : ''}}"
|
||||
data-path="{{item.path}}"
|
||||
data-title="{{item.title}}"
|
||||
data-available="{{item.available}}"
|
||||
bindtap="onTapItem">
|
||||
bindtap="{{item.skeleton ? '' : 'onTapItem'}}">
|
||||
<view class="cat-card__thumb">
|
||||
<image
|
||||
wx:if="{{item.previewImg}}"
|
||||
class="cat-card__thumb-img"
|
||||
src="{{item.previewImg}}"
|
||||
mode="aspectFill" />
|
||||
<text wx:else class="cat-card__thumb-icon"
|
||||
<text
|
||||
wx:elif="{{!item.skeleton}}"
|
||||
class="cat-card__thumb-icon"
|
||||
>{{item.icon}}</text
|
||||
>
|
||||
</view>
|
||||
@@ -76,7 +78,9 @@
|
||||
<text class="cat-card__subtitle"
|
||||
>{{item.subtitle}}</text
|
||||
>
|
||||
<view class="cat-card__tags">
|
||||
<view
|
||||
wx:if="{{!item.skeleton}}"
|
||||
class="cat-card__tags">
|
||||
<text class="tag tag--age"
|
||||
>{{item.ageBand}}</text
|
||||
>
|
||||
@@ -84,7 +88,9 @@
|
||||
>{{item.difficultyLabel}}</text
|
||||
>
|
||||
</view>
|
||||
<view class="cat-card__footer">
|
||||
<view
|
||||
wx:if="{{!item.skeleton}}"
|
||||
class="cat-card__footer">
|
||||
<view class="cat-card__stats">
|
||||
<view class="cat-card__stat">
|
||||
<toy-icon
|
||||
@@ -109,7 +115,7 @@
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:if="{{displayItems.length === 0}}"
|
||||
wx:if="{{displayItems.length === 0 && !loading}}"
|
||||
class="cat-empty">
|
||||
<text class="cat-empty__icon">🔍</text>
|
||||
<text class="cat-empty__text">没有找到匹配的内容</text>
|
||||
|
||||
@@ -12,7 +12,7 @@ export type HomeDisplayItem = {
|
||||
difficulty: '入门' | '基础' | '进阶' | '挑战';
|
||||
icon: string;
|
||||
/** 横向滑动卡片顶部图,有则优先展示,可与 icon 二选一 */
|
||||
img?: string;
|
||||
previewImg?: string;
|
||||
badge?: string;
|
||||
path: string;
|
||||
available: boolean;
|
||||
@@ -139,7 +139,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
||||
difficulty: '基础',
|
||||
icon: '➕',
|
||||
badge: '热门',
|
||||
img: '/assets/entrancePicture/math/addition-5.png',
|
||||
previewImg: '/assets/entrancePicture/math/addition-5.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition',
|
||||
available: true,
|
||||
source: '现有加减法页面可承接',
|
||||
@@ -160,7 +160,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
||||
ageBand: '3-4岁',
|
||||
difficulty: '入门',
|
||||
icon: '🔢',
|
||||
img: '/assets/entrancePicture/math/number-find.png',
|
||||
previewImg: '/assets/entrancePicture/math/number-find.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||
available: true,
|
||||
source: '现有 找数字/写数字 能力',
|
||||
@@ -173,7 +173,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-5岁',
|
||||
difficulty: '基础',
|
||||
icon: '🧮',
|
||||
img: '/assets/entrancePicture/math/count-match.png',
|
||||
previewImg: '/assets/entrancePicture/math/count-match.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||
available: true,
|
||||
source: '现有 countMatch/countingSelect',
|
||||
@@ -186,7 +186,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
||||
ageBand: '5-7岁',
|
||||
difficulty: '基础',
|
||||
icon: '➕',
|
||||
img: '/assets/entrancePicture/math/addition-5.png',
|
||||
previewImg: '/assets/entrancePicture/math/addition-5.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition',
|
||||
available: true,
|
||||
source: '现有 addition/calculationPractice',
|
||||
@@ -355,7 +355,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-6岁',
|
||||
difficulty: '基础',
|
||||
icon: '🧵',
|
||||
img: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
previewImg: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||
available: true,
|
||||
source: '现有 dotConnect/matchConnect',
|
||||
@@ -435,7 +435,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
difficulty: '基础',
|
||||
icon: '➕',
|
||||
badge: 'HOT',
|
||||
img: '/assets/entrancePicture/math/addition-5.png',
|
||||
previewImg: '/assets/entrancePicture/math/addition-5.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition',
|
||||
available: true,
|
||||
source: '现有功能清单 addition',
|
||||
@@ -464,7 +464,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
difficulty: '基础',
|
||||
icon: '🔗',
|
||||
badge: '热门',
|
||||
img: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
previewImg: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||
available: true,
|
||||
source: '现有功能清单 dot-connect',
|
||||
@@ -480,7 +480,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
difficulty: '入门',
|
||||
icon: '🔢',
|
||||
badge: '热门',
|
||||
img: '/assets/entrancePicture/math/number-find.png',
|
||||
previewImg: '/assets/entrancePicture/math/number-find.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||
available: true,
|
||||
source: 'number-find',
|
||||
@@ -494,7 +494,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
difficulty: '基础',
|
||||
icon: '🧮',
|
||||
badge: '热门',
|
||||
img: '/assets/entrancePicture/math/count-match.png',
|
||||
previewImg: '/assets/entrancePicture/math/count-match.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||
available: true,
|
||||
source: 'counting-matching',
|
||||
@@ -508,7 +508,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
difficulty: '进阶',
|
||||
icon: '🟨',
|
||||
badge: '热门',
|
||||
img: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing',
|
||||
available: true,
|
||||
source: 'grid-drawing-5x5',
|
||||
@@ -529,7 +529,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '3-4岁',
|
||||
difficulty: '入门',
|
||||
icon: '🔢',
|
||||
img: '/assets/entrancePicture/math/number-find.png',
|
||||
previewImg: '/assets/entrancePicture/math/number-find.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||
available: true,
|
||||
source: 'number-find / number-write',
|
||||
@@ -542,7 +542,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-5岁',
|
||||
difficulty: '基础',
|
||||
icon: '🧮',
|
||||
img: '/assets/entrancePicture/math/count-match.png',
|
||||
previewImg: '/assets/entrancePicture/math/count-match.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||
available: true,
|
||||
source: 'countMatch / countingSelect',
|
||||
@@ -555,7 +555,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-5岁',
|
||||
difficulty: '基础',
|
||||
icon: '📈',
|
||||
img: '/assets/entrancePicture/math/number-sort.png',
|
||||
previewImg: '/assets/entrancePicture/math/number-sort.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-sort',
|
||||
available: true,
|
||||
source: 'numberSort / missingNumber',
|
||||
@@ -568,7 +568,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '5-7岁',
|
||||
difficulty: '基础',
|
||||
icon: '➕',
|
||||
img: '/assets/entrancePicture/math/addition-5.png',
|
||||
previewImg: '/assets/entrancePicture/math/addition-5.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition',
|
||||
available: true,
|
||||
source: 'addition / calculationPractice',
|
||||
@@ -581,7 +581,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '7-8岁',
|
||||
difficulty: '挑战',
|
||||
icon: '✖️',
|
||||
img: '/assets/entrancePicture/math/multiplication-table.png',
|
||||
previewImg: '/assets/entrancePicture/math/multiplication-table.png',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
|
||||
available: true,
|
||||
source: 'multiplicationTable',
|
||||
@@ -646,7 +646,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-6岁',
|
||||
difficulty: '基础',
|
||||
icon: '🔗',
|
||||
img: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
previewImg: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||
available: true,
|
||||
source: 'dot-connect',
|
||||
@@ -659,7 +659,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-6岁',
|
||||
difficulty: '基础',
|
||||
icon: '🧵',
|
||||
img: '/assets/entrancePicture/focus/match-connect.png',
|
||||
previewImg: '/assets/entrancePicture/focus/match-connect.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||
available: true,
|
||||
source: 'match-connect',
|
||||
@@ -672,7 +672,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-6岁',
|
||||
difficulty: '基础',
|
||||
icon: '🌈',
|
||||
img: '/assets/entrancePicture/focus/color-pattern.png',
|
||||
previewImg: '/assets/entrancePicture/focus/color-pattern.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||
available: true,
|
||||
source: 'color-pattern',
|
||||
@@ -685,7 +685,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '5-7岁',
|
||||
difficulty: '进阶',
|
||||
icon: '🧠',
|
||||
img: '/assets/entrancePicture/focus/grid-reasoning.png',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-reasoning.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||
available: true,
|
||||
source: 'grid-reasoning',
|
||||
@@ -698,7 +698,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '进阶',
|
||||
icon: '🟨',
|
||||
img: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing',
|
||||
available: true,
|
||||
source: 'grid-drawing',
|
||||
@@ -719,7 +719,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '入门',
|
||||
icon: '',
|
||||
img: '/assets/entrancePicture/english/letter-tracing-single.jpg',
|
||||
previewImg: '/assets/entrancePicture/english/letter-tracing-single.jpg',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
||||
available: true,
|
||||
source: 'letter-tracing-single',
|
||||
@@ -732,7 +732,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '入门',
|
||||
icon: '',
|
||||
img: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg',
|
||||
previewImg: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower',
|
||||
available: true,
|
||||
source: 'letter-tracing-upper-lower',
|
||||
@@ -745,7 +745,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '入门',
|
||||
icon: '',
|
||||
img: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg',
|
||||
previewImg: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing',
|
||||
available: true,
|
||||
source: 'letter-tracing-case-pairing',
|
||||
@@ -758,7 +758,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '入门',
|
||||
icon: '',
|
||||
img: '/assets/entrancePicture/english/letter-tracing-two-column.jpg',
|
||||
previewImg: '/assets/entrancePicture/english/letter-tracing-two-column.jpg',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column',
|
||||
available: true,
|
||||
source: 'letter-tracing-two-column',
|
||||
@@ -771,7 +771,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '入门',
|
||||
icon: '',
|
||||
img: '/assets/entrancePicture/english/letter-tracing-half.jpg',
|
||||
previewImg: '/assets/entrancePicture/english/letter-tracing-half.jpg',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half',
|
||||
available: true,
|
||||
source: 'letter-tracing-half',
|
||||
@@ -784,7 +784,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
||||
ageBand: '4-7岁',
|
||||
difficulty: '入门',
|
||||
icon: '',
|
||||
img: '/assets/entrancePicture/english/letter-tracing-three.jpg',
|
||||
previewImg: '/assets/entrancePicture/english/letter-tracing-three.jpg',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three',
|
||||
available: true,
|
||||
source: 'letter-tracing-three',
|
||||
|
||||
@@ -83,6 +83,15 @@
|
||||
border-top-left-radius: 32rpx;
|
||||
border-top-right-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
border-bottom: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
background: #f5edd8;
|
||||
}
|
||||
|
||||
.home-hero-card__cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.home-hero-card__icon {
|
||||
@@ -216,6 +225,8 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
border-bottom: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
background: #f5edd8;
|
||||
}
|
||||
|
||||
.home-track-card__cover {
|
||||
@@ -242,7 +253,6 @@
|
||||
padding: 20rpx @page-padding-inner-x 22rpx;
|
||||
background: #ffffff;
|
||||
white-space: normal;
|
||||
border-top: 1rpx solid #c5c5c5;
|
||||
}
|
||||
|
||||
.home-track-card__title {
|
||||
@@ -264,7 +274,7 @@
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: @text-gray;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
@@ -350,12 +360,22 @@
|
||||
|
||||
.home-hot-card__thumb {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border-radius: 24rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
background: #f5edd8;
|
||||
}
|
||||
|
||||
.home-hot-card__thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.home-hot-card__thumb-emoji {
|
||||
@@ -367,6 +387,9 @@
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
align-self: stretch;
|
||||
padding: 6rpx 0;
|
||||
}
|
||||
|
||||
.home-hot-card__title {
|
||||
@@ -438,4 +461,71 @@
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// ── Skeleton ──
|
||||
@keyframes home-skeleton-pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.home-hero-card--skeleton {
|
||||
pointer-events: none;
|
||||
|
||||
.home-hero-card__top {
|
||||
animation: home-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.home-hero-card__title,
|
||||
.home-hero-card__sub {
|
||||
background: #e8e2d5;
|
||||
border-radius: 8rpx;
|
||||
color: transparent;
|
||||
animation: home-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.home-hero-card__title {
|
||||
width: 50%;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.home-hero-card__sub {
|
||||
width: 70%;
|
||||
height: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.home-hot-card--skeleton {
|
||||
pointer-events: none;
|
||||
|
||||
.home-hot-card__thumb {
|
||||
animation: home-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.home-hot-card__title,
|
||||
.home-hot-card__desc {
|
||||
background: #e8e2d5;
|
||||
border-radius: 8rpx;
|
||||
color: transparent;
|
||||
animation: home-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.home-hot-card__title {
|
||||
width: 40%;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.home-hot-card__desc {
|
||||
width: 65%;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
HOME_CURRENT_CAPABILITY_DATA,
|
||||
type HomeDisplayItem,
|
||||
type HomeDisplaySection,
|
||||
type HomeDisplayDataset,
|
||||
} from './home.data';
|
||||
|
||||
type AgeBandKey = string;
|
||||
@@ -21,7 +22,7 @@ type HomeAgeBandView = {
|
||||
};
|
||||
|
||||
type HomeDisplayItemView = HomeDisplayItem & {
|
||||
topBg: string;
|
||||
skeleton?: boolean;
|
||||
};
|
||||
|
||||
type HomeDisplaySectionView = HomeDisplaySection & {
|
||||
@@ -31,14 +32,6 @@ type HomeDisplaySectionView = HomeDisplaySection & {
|
||||
|
||||
const AGE_BAND_ICONS = ['🌱', '🚀', '🎓', '🧠', '🏆'] as const;
|
||||
|
||||
const CATEGORY_TOP_BG: Record<HomeDisplayItem['category'], string> = {
|
||||
math: '#fff3c4',
|
||||
chinese: '#d8f0d0',
|
||||
english: '#e8f4ff',
|
||||
puzzle: '#f5e6dc',
|
||||
craft: '#fce4ec',
|
||||
};
|
||||
|
||||
const SECTION_ANCHOR_PREFIX = 'section-';
|
||||
const TAB_BAR_PATHS = new Set([
|
||||
'/pages/home/home',
|
||||
@@ -50,7 +43,6 @@ const TAB_BAR_PATHS = new Set([
|
||||
function toItemView(item: HomeDisplayItem): HomeDisplayItemView {
|
||||
return {
|
||||
...item,
|
||||
topBg: CATEGORY_TOP_BG[item.category],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,12 +56,7 @@ function sortSectionsByTabs(
|
||||
}, {});
|
||||
|
||||
return [...sections]
|
||||
.sort((a, b) => {
|
||||
const aOrder = orderMap[a.id];
|
||||
const bOrder = orderMap[b.id];
|
||||
// 注意:0 需要保留(不能用 || 进行兜底)
|
||||
return (aOrder ?? 999) - (bOrder ?? 999);
|
||||
})
|
||||
.sort((a, b) => (orderMap[a.id] ?? 999) - (orderMap[b.id] ?? 999))
|
||||
.map((section) => ({
|
||||
...section,
|
||||
anchorId: `${SECTION_ANCHOR_PREFIX}${section.id}`,
|
||||
@@ -78,6 +65,7 @@ function sortSectionsByTabs(
|
||||
}
|
||||
|
||||
function navigateByPath(path?: string, fallbackTitle?: string) {
|
||||
console.log('path-----:', path);
|
||||
if (!path) {
|
||||
wx.showToast({
|
||||
title: fallbackTitle ? `${fallbackTitle} 即将上线` : '即将上线',
|
||||
@@ -94,31 +82,109 @@ function navigateByPath(path?: string, fallbackTitle?: string) {
|
||||
wx.navigateTo({ url: path });
|
||||
}
|
||||
|
||||
// ── Skeleton data ──
|
||||
|
||||
function skeletonItem(id: string): HomeDisplayItemView {
|
||||
return {
|
||||
id,
|
||||
title: '\u00A0',
|
||||
subtitle: '\u00A0',
|
||||
category: 'math' as any,
|
||||
ageBand: '',
|
||||
difficulty: '基础',
|
||||
icon: '',
|
||||
path: '',
|
||||
available: false,
|
||||
skeleton: true,
|
||||
};
|
||||
}
|
||||
|
||||
const SKELETON_FEATURED = Array.from({ length: 3 }, (_, i) =>
|
||||
skeletonItem(`skeleton-featured-${i}`),
|
||||
);
|
||||
const SKELETON_HOT = Array.from({ length: 3 }, (_, i) =>
|
||||
skeletonItem(`skeleton-hot-${i}`),
|
||||
);
|
||||
|
||||
const win = wx.getWindowInfo();
|
||||
const HOME_DATA = HOME_CURRENT_CAPABILITY_DATA;
|
||||
const HOME_TABS: HomeTab[] = HOME_DATA.categoryTabs.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}));
|
||||
const HOME_SECTIONS = sortSectionsByTabs(HOME_TABS, HOME_DATA.sections);
|
||||
console.log('HOME_DATA', HOME_DATA);
|
||||
console.log('HOME_SECTIONS', HOME_SECTIONS);
|
||||
|
||||
// Page-level mutable state
|
||||
let _homeData: HomeDisplayDataset | null = null;
|
||||
|
||||
function applyHomeData(data: HomeDisplayDataset) {
|
||||
_homeData = data;
|
||||
const tabs: HomeTab[] = data.categoryTabs.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}));
|
||||
const sections = sortSectionsByTabs(tabs, data.sections);
|
||||
const ageBands: HomeAgeBandView[] = data.ageBands.map((item, idx) => ({
|
||||
...item,
|
||||
icon: AGE_BAND_ICONS[idx] || '🌟',
|
||||
}));
|
||||
|
||||
return { tabs, sections, ageBands };
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
searchPlaceholder: HOME_DATA.searchPlaceholder,
|
||||
tabs: HOME_TABS,
|
||||
searchPlaceholder: '搜索你喜欢的练习册...',
|
||||
tabs: [] as HomeTab[],
|
||||
activeTab: 'all',
|
||||
ageBands: HOME_DATA.ageBands.map((item, idx) => ({
|
||||
...item,
|
||||
icon: AGE_BAND_ICONS[idx] || '🌟',
|
||||
})) as HomeAgeBandView[],
|
||||
activeAgeBand: HOME_DATA.ageBands[0]?.key || ('3-4' as AgeBandKey),
|
||||
featuredItems: HOME_DATA.featured.map(toItemView),
|
||||
hotRecommends: HOME_DATA.hot.map(toItemView),
|
||||
sections: HOME_SECTIONS,
|
||||
ageBands: [] as HomeAgeBandView[],
|
||||
activeAgeBand: '3-4' as AgeBandKey,
|
||||
featuredItems: SKELETON_FEATURED,
|
||||
hotRecommends: SKELETON_HOT,
|
||||
sections: [] as HomeDisplaySectionView[],
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
void this.initData();
|
||||
},
|
||||
|
||||
async initData() {
|
||||
wx.showLoading({ title: '加载中', mask: false });
|
||||
|
||||
const app = getApp<IAppOption>();
|
||||
const config = await app.getPageConfig();
|
||||
|
||||
let homeData: HomeDisplayDataset;
|
||||
|
||||
if (config?.home) {
|
||||
// Cloud config available — map to HomeDisplayDataset
|
||||
const raw = config.home as Record<string, any>;
|
||||
homeData = {
|
||||
searchPlaceholder:
|
||||
raw.searchPlaceholder || '搜索你喜欢的练习册...',
|
||||
categoryTabs: raw.categoryTabs || [],
|
||||
ageBands: raw.ageBands || [],
|
||||
featured: (raw.featured || []) as HomeDisplayItem[],
|
||||
hot: (raw.hot || []) as HomeDisplayItem[],
|
||||
sections: (raw.sections || []) as HomeDisplaySection[],
|
||||
};
|
||||
} else {
|
||||
homeData = HOME_CURRENT_CAPABILITY_DATA;
|
||||
}
|
||||
|
||||
const { tabs, sections, ageBands } = applyHomeData(homeData);
|
||||
|
||||
this.setData({
|
||||
loading: false,
|
||||
searchPlaceholder: homeData.searchPlaceholder,
|
||||
tabs,
|
||||
ageBands,
|
||||
activeAgeBand: homeData.ageBands[0]?.key || '3-4',
|
||||
featuredItems: homeData.featured.map(toItemView),
|
||||
hotRecommends: homeData.hot.slice(0, 3).map(toItemView),
|
||||
sections,
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
},
|
||||
|
||||
// ── Tab & Navigation ──
|
||||
|
||||
onTabChange(e: WechatMiniprogram.CustomEvent) {
|
||||
const id = e.detail.id as string | undefined;
|
||||
if (!id) return;
|
||||
@@ -126,10 +192,7 @@ Page({
|
||||
this.setData({ activeTab: id });
|
||||
|
||||
if (id === 'all') {
|
||||
wx.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: 400,
|
||||
});
|
||||
wx.pageScrollTo({ scrollTop: 0, duration: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,21 @@
|
||||
<view
|
||||
wx:for="{{featuredItems}}"
|
||||
wx:key="id"
|
||||
class="home-hero-card"
|
||||
class="home-hero-card {{item.skeleton ? 'home-hero-card--skeleton' : ''}}"
|
||||
data-title="{{item.title}}"
|
||||
data-path="{{item.path}}"
|
||||
bindtap="onTapFeatured">
|
||||
<view
|
||||
class="home-hero-card__top"
|
||||
style="background-color: {{item.topBg}}">
|
||||
<text class="home-hero-card__icon">{{item.icon}}</text>
|
||||
bindtap="{{item.skeleton ? '' : 'onTapFeatured'}}">
|
||||
<view class="home-hero-card__top">
|
||||
<image
|
||||
wx:if="{{!item.skeleton && item.previewImg}}"
|
||||
class="home-hero-card__cover"
|
||||
src="{{item.previewImg}}"
|
||||
mode="aspectFill" />
|
||||
<text
|
||||
wx:if="{{!item.skeleton && !item.previewImg}}"
|
||||
class="home-hero-card__icon"
|
||||
>{{item.icon}}</text
|
||||
>
|
||||
<text
|
||||
wx:if="{{item.badge}}"
|
||||
class="home-hero-card__badge"
|
||||
@@ -46,7 +53,9 @@
|
||||
<text class="home-hero-card__title"
|
||||
>{{item.title}}</text
|
||||
>
|
||||
<text class="home-hero-card__meta"
|
||||
<text
|
||||
wx:if="{{!item.skeleton}}"
|
||||
class="home-hero-card__meta"
|
||||
>{{item.ageBand}} · {{item.difficulty}}</text
|
||||
>
|
||||
</view>
|
||||
@@ -98,14 +107,19 @@
|
||||
<view
|
||||
wx:for="{{hotRecommends}}"
|
||||
wx:key="id"
|
||||
class="home-hot-card"
|
||||
class="home-hot-card {{item.skeleton ? 'home-hot-card--skeleton' : ''}}"
|
||||
data-title="{{item.title}}"
|
||||
data-path="{{item.path}}"
|
||||
bindtap="onTapHotRecommend">
|
||||
<view
|
||||
class="home-hot-card__thumb"
|
||||
style="background-color: {{item.topBg}}">
|
||||
<text class="home-hot-card__thumb-emoji"
|
||||
bindtap="{{item.skeleton ? '' : 'onTapHotRecommend'}}">
|
||||
<view class="home-hot-card__thumb">
|
||||
<image
|
||||
wx:if="{{!item.skeleton && item.previewImg}}"
|
||||
class="home-hot-card__thumb-img"
|
||||
src="{{item.previewImg}}"
|
||||
mode="aspectFill" />
|
||||
<text
|
||||
wx:if="{{!item.skeleton && !item.previewImg}}"
|
||||
class="home-hot-card__thumb-emoji"
|
||||
>{{item.icon}}</text
|
||||
>
|
||||
</view>
|
||||
@@ -114,7 +128,9 @@
|
||||
<text class="home-hot-card__desc"
|
||||
>{{item.subtitle}}</text
|
||||
>
|
||||
<view class="home-hot-card__meta">
|
||||
<view
|
||||
wx:if="{{!item.skeleton}}"
|
||||
class="home-hot-card__meta">
|
||||
<text class="home-hot-card__tag"
|
||||
>{{item.ageBand}}</text
|
||||
>
|
||||
@@ -123,7 +139,11 @@
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="home-hot-card__action">›</view>
|
||||
<view
|
||||
wx:if="{{!item.skeleton}}"
|
||||
class="home-hot-card__action"
|
||||
>›</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -157,13 +177,11 @@
|
||||
data-title="{{item.title}}"
|
||||
data-path="{{item.path}}"
|
||||
bindtap="onTapTrackCard">
|
||||
<view
|
||||
class="home-track-card__top"
|
||||
style="background-color: {{item.topBg}}">
|
||||
<view class="home-track-card__top">
|
||||
<image
|
||||
wx:if="{{item.img}}"
|
||||
wx:if="{{item.previewImg}}"
|
||||
class="home-track-card__cover"
|
||||
src="{{item.img}}"
|
||||
src="{{item.previewImg}}"
|
||||
mode="widthFix" />
|
||||
<text wx:else class="home-track-card__icon"
|
||||
>{{item.icon}}</text
|
||||
|
||||