372 lines
12 KiB
TypeScript
372 lines
12 KiB
TypeScript
import { CATEGORY_LIST } from '../../core/data/categories';
|
|
|
|
type SimpleWorksheet = {
|
|
_id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
category: string;
|
|
previewImg: string;
|
|
ageMin: number;
|
|
ageMax: number;
|
|
difficulty: number;
|
|
isNew: boolean;
|
|
isHot: boolean;
|
|
path: string;
|
|
ageBand: string;
|
|
downloads: number;
|
|
likes: number;
|
|
};
|
|
|
|
type SectionConfig = {
|
|
id: string;
|
|
name: string;
|
|
icon: string;
|
|
subtitle?: string;
|
|
morePath?: string;
|
|
items: SimpleWorksheet[];
|
|
};
|
|
|
|
type HomeConfigItem = {
|
|
id: string;
|
|
title?: string;
|
|
subtitle?: string;
|
|
morePath?: string;
|
|
items: Array<{ id: string; path?: string; [key: string]: unknown }>;
|
|
};
|
|
|
|
type PageConfig = {
|
|
home?: {
|
|
featured?: Array<{ id: string; [key: string]: unknown }>;
|
|
hot?: Array<{ id: string; [key: string]: unknown }>;
|
|
sections?: HomeConfigItem[];
|
|
};
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
type CloudFunctionResult<T> = {
|
|
success?: boolean;
|
|
message?: string;
|
|
data?: T;
|
|
};
|
|
|
|
function formatSimple(raw: Record<string, unknown>): SimpleWorksheet {
|
|
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 || ''),
|
|
category: String(raw.category || ''),
|
|
previewImg: String(raw.previewImg || ''),
|
|
ageMin,
|
|
ageMax,
|
|
difficulty: Number(raw.difficulty) || 2,
|
|
isNew: !!raw.isNew,
|
|
isHot: !!raw.isHot,
|
|
path: String(raw.path || ''),
|
|
ageBand: `${ageMin}-${ageMax}岁`,
|
|
downloads: Number(raw.downloads) || 0,
|
|
likes: Number(raw.likes) || 0,
|
|
};
|
|
}
|
|
|
|
async function callCloudFunction<T>(
|
|
name: string,
|
|
data?: Record<string, unknown>,
|
|
): Promise<CloudFunctionResult<T>> {
|
|
const response = (await wx.cloud.callFunction({
|
|
name,
|
|
data: data || {},
|
|
})) as { result?: CloudFunctionResult<T> };
|
|
return response.result || {};
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
featured: [] as SimpleWorksheet[],
|
|
hot: [] as SimpleWorksheet[],
|
|
sections: [] as SectionConfig[],
|
|
// worksheet pool (all active)
|
|
allActive: [] as SimpleWorksheet[],
|
|
// selector popup
|
|
selectorVisible: false,
|
|
selectorTarget: '', // 'featured' | 'hot' | category id
|
|
selectorTargetLabel: '',
|
|
selectorItems: [] as SimpleWorksheet[],
|
|
// timer
|
|
timerEnabled: true,
|
|
timerLoading: false,
|
|
// states
|
|
loading: false,
|
|
building: false,
|
|
},
|
|
|
|
onLoad() {
|
|
this.setData({
|
|
sections: CATEGORY_LIST.map((c) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
icon: c.icon,
|
|
items: [],
|
|
})),
|
|
});
|
|
void this.loadData();
|
|
void this.loadTimerStatus();
|
|
},
|
|
|
|
async onPullDownRefresh() {
|
|
await this.loadData();
|
|
await this.loadTimerStatus();
|
|
wx.stopPullDownRefresh();
|
|
},
|
|
|
|
async loadTimerStatus() {
|
|
try {
|
|
const db = wx.cloud.database();
|
|
const { data } = await db
|
|
.collection('settings')
|
|
.doc('homeAutoRefresh')
|
|
.get();
|
|
this.setData({ timerEnabled: (data as { enabled?: boolean }).enabled !== false });
|
|
} catch {
|
|
// Document doesn't exist — default enabled
|
|
this.setData({ timerEnabled: true });
|
|
}
|
|
},
|
|
|
|
async loadData() {
|
|
this.setData({ loading: true });
|
|
try {
|
|
// Fetch in parallel: active worksheets + saved page config
|
|
const [wsResult, configResult] = await Promise.all([
|
|
callCloudFunction<SimpleWorksheet[]>('worksheetsQuery', {
|
|
status: 'active',
|
|
}),
|
|
callCloudFunction<PageConfig>('pageConfigFetch'),
|
|
]);
|
|
|
|
if (!wsResult.success) throw new Error(wsResult.message || '查询失败');
|
|
|
|
const allActive = (wsResult.data || []).map((raw) =>
|
|
formatSimple(raw as unknown as Record<string, unknown>),
|
|
);
|
|
|
|
// Build a lookup map for quick access
|
|
const wsMap = new Map(allActive.map((w) => [w._id, w]));
|
|
|
|
// featured: always top-5 by downloads; hot: always top-5 by likes
|
|
const featured: SimpleWorksheet[] = [...allActive]
|
|
.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0))
|
|
.slice(0, 5);
|
|
const hot: SimpleWorksheet[] = [...allActive]
|
|
.sort((a, b) => (b.likes ?? 0) - (a.likes ?? 0))
|
|
.slice(0, 5);
|
|
|
|
// Restore saved sections from config
|
|
const savedHome = (configResult.data as PageConfig | undefined)?.home;
|
|
const savedSections: HomeConfigItem[] = savedHome?.sections || [];
|
|
const savedSectionMap = new Map(savedSections.map((s) => [s.id, s]));
|
|
|
|
const sections: SectionConfig[] = CATEGORY_LIST.map((c) => {
|
|
const saved = savedSectionMap.get(c.id);
|
|
const items = saved
|
|
? (saved.items || [])
|
|
.map((item) => wsMap.get(item.id))
|
|
.filter((w): w is SimpleWorksheet => !!w)
|
|
: [];
|
|
return {
|
|
id: c.id,
|
|
name: c.name,
|
|
icon: c.icon,
|
|
subtitle: saved?.subtitle || '',
|
|
morePath: c.path,
|
|
items,
|
|
};
|
|
});
|
|
|
|
this.setData({ loading: false, allActive, featured, hot, sections });
|
|
} catch (error) {
|
|
this.setData({ loading: false });
|
|
wx.showToast({
|
|
title: error instanceof Error ? error.message : '加载失败',
|
|
icon: 'none',
|
|
});
|
|
}
|
|
},
|
|
|
|
// ── Timer Control ──
|
|
|
|
async onToggleTimer() {
|
|
if (this.data.timerLoading) return;
|
|
const newEnabled = !this.data.timerEnabled;
|
|
|
|
this.setData({ timerLoading: true });
|
|
try {
|
|
const result = await callCloudFunction('homeTimerControl', {
|
|
enabled: newEnabled,
|
|
});
|
|
if (!result.success) {
|
|
throw new Error(result.message || '操作失败');
|
|
}
|
|
this.setData({ timerEnabled: newEnabled });
|
|
wx.showToast({
|
|
title: newEnabled ? '定时任务已启动' : '定时任务已暂停',
|
|
icon: 'success',
|
|
});
|
|
} catch (error) {
|
|
wx.showToast({
|
|
title: error instanceof Error ? error.message : '操作失败',
|
|
icon: 'none',
|
|
});
|
|
} finally {
|
|
this.setData({ timerLoading: false });
|
|
}
|
|
},
|
|
|
|
// ── Selector ──
|
|
|
|
onTapAdd(e: WechatMiniprogram.TouchEvent) {
|
|
const target = e.currentTarget.dataset.target as string;
|
|
if (!target) return;
|
|
|
|
let label = '';
|
|
let selectedIds: Set<string>;
|
|
|
|
if (target === 'featured') {
|
|
label = '今日推荐';
|
|
selectedIds = new Set(this.data.featured.map((w) => w._id));
|
|
} else if (target === 'hot') {
|
|
label = '热门推荐';
|
|
selectedIds = new Set(this.data.hot.map((w) => w._id));
|
|
} else {
|
|
const section = this.data.sections.find((s) => s.id === target);
|
|
label = section?.name || target;
|
|
selectedIds = new Set((section?.items || []).map((w) => w._id));
|
|
}
|
|
|
|
// Filter: show active worksheets not already selected
|
|
let pool = this.data.allActive.filter((w) => !selectedIds.has(w._id));
|
|
// For sections, prefer same category
|
|
if (target !== 'featured' && target !== 'hot') {
|
|
pool = pool.filter((w) => w.category === target);
|
|
}
|
|
|
|
this.setData({
|
|
selectorVisible: true,
|
|
selectorTarget: target,
|
|
selectorTargetLabel: label,
|
|
selectorItems: pool,
|
|
});
|
|
},
|
|
|
|
onCloseSelector() {
|
|
this.setData({ selectorVisible: false });
|
|
},
|
|
|
|
onSelectItem(e: WechatMiniprogram.TouchEvent) {
|
|
const id = e.currentTarget.dataset.id as string;
|
|
if (!id) return;
|
|
|
|
const ws = this.data.allActive.find((w) => w._id === id);
|
|
if (!ws) return;
|
|
|
|
const target = this.data.selectorTarget;
|
|
|
|
if (target === 'featured') {
|
|
this.setData({ featured: [...this.data.featured, ws] });
|
|
} else if (target === 'hot') {
|
|
this.setData({ hot: [...this.data.hot, ws] });
|
|
} else {
|
|
const sections = [...this.data.sections];
|
|
const idx = sections.findIndex((s) => s.id === target);
|
|
if (idx >= 0) {
|
|
sections[idx] = {
|
|
...sections[idx],
|
|
items: [...sections[idx].items, ws],
|
|
};
|
|
this.setData({ sections });
|
|
}
|
|
}
|
|
|
|
this.setData({ selectorVisible: false });
|
|
},
|
|
|
|
onRemoveItem(e: WechatMiniprogram.TouchEvent) {
|
|
const target = e.currentTarget.dataset.target as string;
|
|
const id = e.currentTarget.dataset.id as string;
|
|
if (!target || !id) return;
|
|
|
|
if (target === 'featured') {
|
|
this.setData({
|
|
featured: this.data.featured.filter((w) => w._id !== id),
|
|
});
|
|
} else if (target === 'hot') {
|
|
this.setData({
|
|
hot: this.data.hot.filter((w) => w._id !== id),
|
|
});
|
|
} else {
|
|
const sections = [...this.data.sections];
|
|
const idx = sections.findIndex((s) => s.id === target);
|
|
if (idx >= 0) {
|
|
sections[idx] = {
|
|
...sections[idx],
|
|
items: sections[idx].items.filter((w) => w._id !== id),
|
|
};
|
|
this.setData({ sections });
|
|
}
|
|
}
|
|
},
|
|
|
|
// ── Build ──
|
|
|
|
async onBuildTap() {
|
|
if (this.data.building) return;
|
|
|
|
const confirmed = await new Promise<boolean>((resolve) => {
|
|
wx.showModal({
|
|
title: '更新首页数据',
|
|
content: '将根据当前配置重新生成首页数据,确认继续?',
|
|
success: (res) => resolve(!!res.confirm),
|
|
fail: () => resolve(false),
|
|
});
|
|
});
|
|
if (!confirmed) return;
|
|
|
|
this.setData({ building: true });
|
|
|
|
try {
|
|
const result = await callCloudFunction<{
|
|
version: number;
|
|
}>('pageContentBuild', {
|
|
page: 'home',
|
|
featuredIds: this.data.featured.map((w) => w._id),
|
|
hotIds: this.data.hot.map((w) => w._id),
|
|
sections: this.data.sections
|
|
.filter((s) => s.items.length > 0)
|
|
.map((s) => ({
|
|
id: s.id,
|
|
subtitle: s.subtitle || '',
|
|
morePath: s.morePath || '',
|
|
worksheetIds: s.items.map((w) => w._id),
|
|
})),
|
|
});
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.message || '生成失败');
|
|
}
|
|
|
|
wx.showToast({
|
|
title: `生成成功 v${result.data?.version || 0}`,
|
|
icon: 'success',
|
|
});
|
|
} catch (error) {
|
|
wx.showToast({
|
|
title: error instanceof Error ? error.message : '生成失败',
|
|
icon: 'none',
|
|
});
|
|
} finally {
|
|
this.setData({ building: false });
|
|
}
|
|
},
|
|
});
|