import { MOCK_SEED_COUNTS } from './mockLikes'; import { MATH_WORKSHEET_DEFINITIONS, CLOCK_CONNECT_WORKSHEET_DEFINITIONS, FOCUS_WORKSHEET_DEFINITIONS, LETTER_TRACING_WORKSHEET_DEFINITIONS, PINYIN_DICTATION_WORKSHEET_DEFINITIONS, PEN_CONTROL_WORKSHEET_DEFINITIONS, WORD_COLORING_WORKSHEET_DEFINITIONS, type WorksheetDefinition, } from '../../config/worksheets'; type CloudFunctionResult = { success?: boolean; message?: string; data?: T; dryRun?: boolean; total?: number; failed?: number; results?: Array<{ id: string | null; ok: boolean; error?: string }>; }; async function callCloudFunction( name: string, data?: Record, ): Promise> { const response = (await wx.cloud.callFunction({ name, data: data || {}, })) as { result?: CloudFunctionResult }; return response.result || {}; } function inferGradeFromAge(ageMin: number, ageMax: number): number { const centerAge = Math.round((ageMin + ageMax) / 2); const ageGradeMap: Record = { 2: -4, 3: -3, 4: -2, 5: -1, 6: 0, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5, 12: 6, }; return ageGradeMap[centerAge] ?? 0; } type ConfigSource = { label: string; data: ReadonlyArray; }; const CONFIG_SOURCES: ConfigSource[] = [ { label: 'math', data: MATH_WORKSHEET_DEFINITIONS, }, { label: 'focus', data: FOCUS_WORKSHEET_DEFINITIONS, }, { label: 'letterTracing', data: LETTER_TRACING_WORKSHEET_DEFINITIONS, }, { label: 'pinyin', data: PINYIN_DICTATION_WORKSHEET_DEFINITIONS, }, { label: 'penControl', data: PEN_CONTROL_WORKSHEET_DEFINITIONS, }, { label: 'wordColoring', data: WORD_COLORING_WORKSHEET_DEFINITIONS, }, { label: 'clockConnect', data: CLOCK_CONNECT_WORKSHEET_DEFINITIONS, }, ]; function isWorksheetDefinition(value: unknown): value is WorksheetDefinition { const row = value as Partial; return ( !!row && typeof row.id === 'string' && typeof row.ageMin === 'number' && typeof row.ageMax === 'number' && Array.isArray(row.tags) ); } async function loadConfigSource( source: ConfigSource, ): Promise { const rows = source.data; if (!Array.isArray(rows) || !rows.every(isWorksheetDefinition)) { throw new Error(`${source.label} 配置格式不正确`); } return [...rows]; } async function collectAll(): Promise { const groups = await Promise.all(CONFIG_SOURCES.map(loadConfigSource)); return groups.flat(); } function assertUniqueIds(rows: WorksheetDefinition[]) { const set = new Set(); for (const r of rows) { if (set.has(r.id)) { throw new Error(`本地配置存在重复 id:${r.id}`); } set.add(r.id); } } function summarizeResult( title: string, result: CloudFunctionResult, ): string { if (!result.success) { return `${title}失败:${result.message || '未知错误'}`; } const total = result.total ?? 0; const failed = result.failed ?? 0; const ok = total - failed; const firstError = result.results?.find((r) => !r.ok)?.error; return `${title}完成:成功 ${ok}/${total}${ failed ? `,失败 ${failed}(如:${firstError || '未知原因'})` : '' }`; } Page({ data: { configLoading: true, configReady: false, configError: '', configTotal: 0, configSourceCount: CONFIG_SOURCES.length, loadingAges: false, loadingTags: false, loadingRebuild: false, loadingSeed: false, loadingResetStats: false, statusText: '配置加载中,请稍候', }, onLoad() { void this.loadConfigs(); }, async onPullDownRefresh() { await this.loadConfigs(); wx.stopPullDownRefresh(); }, async loadConfigs() { this.setData({ configLoading: true, configReady: false, configError: '', statusText: '配置加载中,请稍候', }); try { const rows = await collectAll(); assertUniqueIds(rows); this.setData({ configLoading: false, configReady: true, configTotal: rows.length, statusText: `配置已加载:${rows.length} 个 worksheet,可执行同步`, }); } catch (error) { const msg = error instanceof Error ? error.message : '配置加载失败'; this.setData({ configLoading: false, configReady: false, configError: msg, statusText: msg, }); wx.showToast({ title: msg, icon: 'none' }); } }, async sync(mode: 'ages' | 'tags') { if (!this.data.configReady) { wx.showToast({ title: '配置未加载完成', icon: 'none' }); return; } const rows = await collectAll(); assertUniqueIds(rows); const patches = mode === 'ages' ? rows.map((r) => ({ id: r.id, ageMin: r.ageMin, ageMax: r.ageMax, grade: inferGradeFromAge(r.ageMin, r.ageMax), })) : rows.map((r) => ({ id: r.id, tags: [...r.tags], })); wx.showLoading({ title: '同步中...' }); const startedAt = Date.now(); try { const patchRes = await callCloudFunction('worksheetsBatchPatch', { patches, dryRun: false, }); if (!patchRes.success) { throw new Error(patchRes.message || '同步失败'); } const elapsed = Math.round((Date.now() - startedAt) / 1000); const text = [ summarizeResult( mode === 'ages' ? '同步年龄(worksheetsBatchPatch)' : '同步标签(worksheetsBatchPatch)', patchRes, ), 'page-config 未自动重建,如需刷新前台展示请点击“重建配置”。', `耗时:${elapsed}s`, ].join('\n'); this.setData({ statusText: text }); wx.showToast({ title: '已完成', icon: 'success' }); } catch (error) { const msg = error instanceof Error ? error.message : '同步失败'; this.setData({ statusText: msg }); wx.showToast({ title: msg, icon: 'none' }); } finally { wx.hideLoading(); } }, async rebuildPageConfig() { wx.showLoading({ title: '重建中...' }); const startedAt = Date.now(); try { const rebuildRes = await callCloudFunction('pageContentBuild', { page: 'all', }); const elapsed = Math.round((Date.now() - startedAt) / 1000); const text = [ summarizeResult( '重建 page-config(pageContentBuild: all)', rebuildRes, ), `耗时:${elapsed}s`, ].join('\n'); this.setData({ statusText: text }); if (!rebuildRes.success) { throw new Error(rebuildRes.message || '重建失败'); } wx.showToast({ title: '已重建', icon: 'success' }); } catch (error) { const msg = error instanceof Error ? error.message : '重建失败'; this.setData({ statusText: msg }); wx.showToast({ title: msg, icon: 'none' }); } finally { wx.hideLoading(); } }, async onSyncAges() { if ( !this.data.configReady || this.data.loadingAges || this.data.loadingTags || this.data.loadingRebuild ) return; this.setData({ loadingAges: true }); try { await this.sync('ages'); } finally { this.setData({ loadingAges: false }); } }, async onSyncTags() { if ( !this.data.configReady || this.data.loadingAges || this.data.loadingTags || this.data.loadingRebuild ) return; this.setData({ loadingTags: true }); try { await this.sync('tags'); } finally { this.setData({ loadingTags: false }); } }, async onRebuildConfig() { if ( this.data.loadingAges || this.data.loadingTags || this.data.loadingRebuild ) return; this.setData({ loadingRebuild: true }); try { await this.rebuildPageConfig(); } finally { this.setData({ loadingRebuild: false }); } }, async onSyncSeed() { if ( this.data.loadingAges || this.data.loadingTags || this.data.loadingRebuild || this.data.loadingSeed ) return; this.setData({ loadingSeed: true }); wx.showLoading({ title: '同步 Seed...' }); const startedAt = Date.now(); try { const patches = Object.entries(MOCK_SEED_COUNTS).map( ([id, counts]) => ({ id, likes_seed: counts.likes, downloads_seed: counts.downloads, }), ); const patchRes = await callCloudFunction('worksheetsBatchPatch', { patches, dryRun: false, }); if (!patchRes.success) { throw new Error(patchRes.message || '同步 Seed 失败'); } const elapsed = Math.round((Date.now() - startedAt) / 1000); const text = [ summarizeResult( '同步 Seed(likes_seed / downloads_seed)', patchRes, ), `共 ${patches.length} 条 Seed 数据`, `耗时:${elapsed}s`, ].join('\n'); this.setData({ statusText: text }); wx.showToast({ title: '已完成', icon: 'success' }); } catch (error) { const msg = error instanceof Error ? error.message : '同步 Seed 失败'; this.setData({ statusText: msg }); wx.showToast({ title: msg, icon: 'none' }); } finally { wx.hideLoading(); this.setData({ loadingSeed: false }); } }, async onResetStats() { if ( this.data.loadingAges || this.data.loadingTags || this.data.loadingRebuild || this.data.loadingSeed || this.data.loadingResetStats ) return; const { confirm } = await wx.showModal({ title: '确认初始化', content: '将所有 worksheet 的 likes 和 downloads 重置为 0,此操作不可撤销。确定继续?', confirmText: '确定重置', confirmColor: '#e53935', }); if (!confirm) return; this.setData({ loadingResetStats: true }); wx.showLoading({ title: '重置中...' }); const startedAt = Date.now(); try { const queryRes = await callCloudFunction>( 'worksheetsQuery', {}, ); if (!queryRes.success || !queryRes.data) { throw new Error(queryRes.message || '查询 worksheet 列表失败'); } const patches = (queryRes.data as Array<{ _id: string }>).map( (ws) => ({ id: ws._id, likes: 0, downloads: 0, }), ); if (patches.length === 0) { this.setData({ statusText: '没有需要重置的 worksheet' }); wx.showToast({ title: '无数据', icon: 'none' }); return; } const patchRes = await callCloudFunction('worksheetsBatchPatch', { patches, dryRun: false, }); if (!patchRes.success) { throw new Error(patchRes.message || '重置失败'); } const elapsed = Math.round((Date.now() - startedAt) / 1000); const text = [ summarizeResult( '初始化收藏和下载数据(likes / downloads → 0)', patchRes, ), `共 ${patches.length} 条 worksheet`, `耗时:${elapsed}s`, ].join('\n'); this.setData({ statusText: text }); wx.showToast({ title: '已重置', icon: 'success' }); } catch (error) { const msg = error instanceof Error ? error.message : '重置失败'; this.setData({ statusText: msg }); wx.showToast({ title: msg, icon: 'none' }); } finally { wx.hideLoading(); this.setData({ loadingResetStats: false }); } }, });