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 = { success?: boolean; message?: string; data?: T; }; function formatSimple(raw: Record): 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( name: string, data?: Record, ): Promise> { const response = (await wx.cloud.callFunction({ name, data: data || {}, })) as { result?: CloudFunctionResult }; 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('worksheetsQuery', { status: 'active', }), callCloudFunction('pageConfigFetch'), ]); if (!wsResult.success) throw new Error(wsResult.message || '查询失败'); const allActive = (wsResult.data || []).map((raw) => formatSimple(raw as unknown as Record), ); // 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; 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((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 }); } }, });