feat: 3.5版本规划

This commit is contained in:
R524809
2026-09-24 17:14:38 +08:00
parent f82ac4d89d
commit 08d9a2792a
19 changed files with 769 additions and 45 deletions
+109 -9
View File
@@ -85,6 +85,7 @@ type SharePathData = {
functionId?: string;
worksheetId?: string;
currentMode?: string;
pageTitle?: string;
};
type SharePathPage = {
@@ -143,9 +144,13 @@ export function getAppShareAppMessage(
trackerLabel?: string,
page?: SharePathPage,
) {
if (trackerLabel) {
tracker.reportShare(trackerLabel);
}
tracker.reportShare(page?.route || trackerLabel || '', {
...(trackerLabel && { worksheet_title: trackerLabel }),
...(page?.data?.functionId || page?.data?.worksheetId
? { worksheet_id: page.data.functionId || page.data.worksheetId }
: {}),
...(page?.data?.currentMode ? { mode: page.data.currentMode } : {}),
});
return {
title: defaultShareConfig.title,
path: buildPageSharePath(page),
@@ -158,9 +163,13 @@ export function getAppShareTimeline(
trackerLabel?: string,
page?: SharePathPage,
) {
if (trackerLabel) {
tracker.reportShare(trackerLabel);
}
tracker.reportShare(page?.route || trackerLabel || '', {
...(trackerLabel && { worksheet_title: trackerLabel }),
...(page?.data?.functionId || page?.data?.worksheetId
? { worksheet_id: page.data.functionId || page.data.worksheetId }
: {}),
...(page?.data?.currentMode ? { mode: page.data.currentMode } : {}),
});
return {
title: defaultShareConfig.title,
query: buildPageShareTimelineQuery(page),
@@ -171,10 +180,10 @@ export function getAppShareTimeline(
/** 供 Page({}) 直接展开:统一转发 / 分享到朋友圈(path 为当前页) */
export const appSharePageMethods = {
onShareAppMessage(this: SharePathPage) {
return getAppShareAppMessage(undefined, this);
return getAppShareAppMessage(this.data?.pageTitle, this);
},
onShareTimeline(this: SharePathPage) {
return getAppShareTimeline(undefined, this);
return getAppShareTimeline(this.data?.pageTitle, this);
},
};
@@ -331,6 +340,7 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
errorToast: '请先生成内容',
trackerName: this.data.pageTitle,
trackerMode: this.data.currentMode,
trackerPage: this.route,
worksheetId,
};
@@ -394,6 +404,7 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
errorToast: '请先生成内容',
trackerName: this.data.pageTitle,
trackerMode: this.data.currentMode,
trackerPage: this.route,
worksheetId,
});
}
@@ -591,6 +602,11 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
});
wx.setNavigationBarTitle({ title });
tracker.reportPreviewView(this.route || '', {
worksheet_id: functionId,
worksheet_title: title,
...(this.data.currentMode ? { mode: this.data.currentMode } : {}),
});
},
};
}
@@ -607,6 +623,90 @@ export function applyPageMixin(
config: PageCommonMethodsConfig = {},
) {
const commonMethods = getPageCommonMethods(config);
const trackedOptions = { ...pageOptions };
const generationStartKey = '__generationTrackingStartedAt';
const beginGenerationTracking = function (this: PageCanvasInstance) {
if ((this as any)[generationStartKey]) return;
const worksheetId =
typeof this.getWorksheetStatsId === 'function'
? this.getWorksheetStatsId()
: this.data.functionId;
(this as any)[generationStartKey] = Date.now();
tracker.reportGenerateStart(this.route || '', {
worksheet_id: worksheetId || '',
...(this.data.currentMode ? { mode: this.data.currentMode } : {}),
});
};
const originalOnRandom = pageOptions.onRandom;
if (typeof originalOnRandom === 'function') {
trackedOptions.onRandom = function (
this: PageCanvasInstance,
...args: any[]
) {
beginGenerationTracking.call(this);
return originalOnRandom.apply(this, args);
};
}
const originalOnPreviewRefresh = pageOptions.onPreviewRefresh;
if (typeof originalOnPreviewRefresh === 'function') {
trackedOptions.onPreviewRefresh = function (
this: PageCanvasInstance,
...args: any[]
) {
beginGenerationTracking.call(this);
return originalOnPreviewRefresh.apply(this, args);
};
}
const originalDrawCanvas = pageOptions.drawCanvas;
if (typeof originalDrawCanvas === 'function') {
trackedOptions.drawCanvas = async function (
this: PageCanvasInstance,
...args: any[]
) {
try {
const result = await originalDrawCanvas.apply(this, args);
const startedAt = (this as any)[generationStartKey] as
| number
| undefined;
if (startedAt) {
delete (this as any)[generationStartKey];
const worksheetId =
typeof this.getWorksheetStatsId === 'function'
? this.getWorksheetStatsId()
: this.data.functionId;
tracker.reportGenerateResult(
this.route || '',
this.data.hasContent !== false,
{
worksheet_id: worksheetId || '',
duration_ms: Date.now() - startedAt,
...(this.data.currentMode
? { mode: this.data.currentMode }
: {}),
},
);
}
return result;
} catch (error) {
const startedAt = (this as any)[generationStartKey] as
| number
| undefined;
if (startedAt) {
delete (this as any)[generationStartKey];
tracker.reportGenerateResult(this.route || '', false, {
worksheet_id: this.data.functionId || '',
duration_ms: Date.now() - startedAt,
fail_reason: 'draw_exception',
});
}
throw error;
}
};
}
// 提取 pageOptions 中的 data(如果有)
const pageData = pageOptions.data || {};
@@ -615,7 +715,7 @@ export function applyPageMixin(
// 注意:pageOptions 放在后面,这样页面可以覆盖公共方法
const mergedOptions: any = {
...commonMethods,
...pageOptions,
...trackedOptions,
// 处理 data 的合并(需要特殊处理,避免覆盖)
data: {
...pageData,
@@ -55,6 +55,15 @@ createFocusPage({
onLoad(options: { id?: string; mode?: string }) {
const routeId = options.id || FOCUS_TYPE_CONFIGS[0].id;
// “识别形状”仍由独立页面承载。兼容已发布的旧路径和本地兜底
// /focusPages/focusDraw/focusDraw?id=shape-recognition,避免通用注册表
// 找不到该 id 时静默回退为第一个题型。
if (routeId === 'shape-recognition') {
wx.redirectTo({ url: '/focusPages/shape/shape' });
return;
}
const result = findTypeByRouteId(routeId);
if (!result) return;
+19
View File
@@ -10,6 +10,7 @@ import {
shouldShowShareGuide,
shouldUnlockWithAd,
} from '../../utils/shareGuide';
import tracker from '../../utils/tracker';
Page({
canvas: null as Canvas | null,
@@ -20,6 +21,8 @@ Page({
timer: null as any,
data: {
worksheetId: 'shape-recognition',
pageTitle: '识别图形,涂一涂',
shapeList: [] as ShapeCard[],
showSelectShapePopup: false,
selectedShapeIndex: -1,
@@ -31,6 +34,10 @@ Page({
},
onLoad() {
tracker.reportPreviewView('focusPages/shape/shape', {
worksheet_id: 'shape-recognition',
worksheet_title: '识别图形,涂一涂',
});
// 判断是否在PC端开发工具上运行
// const systemInfo = wx.getDeviceInfo();
// if (systemInfo.platform === 'devtools') {
@@ -213,6 +220,10 @@ Page({
* 随机生成6个图形
*/
refreshShapeCard() {
const startedAt = Date.now();
tracker.reportGenerateStart('focusPages/shape/shape', {
worksheet_id: 'shape-recognition',
});
// 随机取6个图形
const shapesCopy = [...SHAPES];
for (let i = shapesCopy.length - 1; i > 0; i--) {
@@ -244,6 +255,10 @@ Page({
() => {
// 绘制canvas
this.drawCanvas();
tracker.reportGenerateResult('focusPages/shape/shape', true, {
worksheet_id: 'shape-recognition',
duration_ms: Date.now() - startedAt,
});
},
);
},
@@ -312,6 +327,8 @@ Page({
await downloadPrint(this.canvas, {
errorToast: '请先生成图形',
trackerName: '图形涂色',
trackerPage: 'focusPages/shape/shape',
worksheetId: 'shape-recognition',
});
},
@@ -327,6 +344,8 @@ Page({
await downloadPrint(this.canvas, {
errorToast: '请先生成图形',
trackerName: '图形涂色',
trackerPage: 'focusPages/shape/shape',
worksheetId: 'shape-recognition',
});
}
},
+1 -1
View File
@@ -443,7 +443,7 @@ const CATEGORY_ITEMS_BY_ID: Record<string, CategoryItem[]> = {
previewImg: '/assets/entrancePicture/focus/shape-recognition.png',
ageBand: age(3, 5),
difficulty: 1,
path: '/focusPages/focusDraw/focusDraw?id=shape-recognition',
path: '/focusPages/shape/shape',
available: true,
}),
item({
+13 -1
View File
@@ -1,6 +1,7 @@
import { appSharePageMethods } from '../../base/pageMixin';
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
import { takePendingCategoryTabId } from '../../utils/index';
import { parseMiniProgramUrl, takePendingCategoryTabId } from '../../utils/index';
import tracker from '../../utils/tracker';
import { CATEGORY_DATA, type CategoryItem } from './category.data';
type CategoryTab = {
@@ -224,6 +225,7 @@ Page({
_defaultCategoryId: 'all',
onLoad(options: Record<string, string>) {
tracker.reportPageView('pages/category/category');
if (options.id && options.id !== 'all') {
this._defaultCategoryId = options.id;
this.setData({ activeCategoryId: options.id });
@@ -285,6 +287,9 @@ Page({
displayItems: filtered,
scrollIntoViewId: '',
});
tracker.report('category_select', 'pages/category/category', {
category_id: id,
});
},
onSearchInput(e: WechatMiniprogram.Input) {
@@ -319,6 +324,13 @@ Page({
return;
}
tracker.reportWorksheetClick('pages/category/category', {
worksheet_id: String(parseMiniProgramUrl(path).query.id || ''),
worksheet_title: title || '',
category_id: this.data.activeCategoryId,
source: 'category_list',
});
if (TAB_BAR_PATHS.has(path)) {
wx.switchTab({ url: path });
return;
+22
View File
@@ -4,6 +4,7 @@ import {
parseMiniProgramUrl,
stashPendingCategoryTabId,
} from '../../utils/index';
import tracker from '../../utils/tracker';
import {
HOME_DAILY_CHECKIN_ITEMS,
HOME_NOTICES,
@@ -124,6 +125,23 @@ function navigateByPath(path?: string, fallbackTitle?: string) {
});
}
function worksheetIdFromPath(path?: string): string {
if (!path) return '';
return String(parseMiniProgramUrl(path).query.id || '');
}
function trackWorksheetClick(
path: string | undefined,
title: string | undefined,
source: string,
) {
tracker.reportWorksheetClick('pages/home/home', {
worksheet_id: worksheetIdFromPath(path),
worksheet_title: title || '',
source,
});
}
// ── Skeleton data ──
function skeletonItem(id: string): HomeDisplayItemView {
@@ -196,6 +214,7 @@ Page({
},
onLoad() {
tracker.reportPageView('pages/home/home');
void this.initData();
},
@@ -274,12 +293,14 @@ Page({
onTapFeatured(e: WechatMiniprogram.TouchEvent) {
const path = e.currentTarget.dataset.path as string | undefined;
const title = e.currentTarget.dataset.title as string | undefined;
trackWorksheetClick(path, title, 'home_featured');
navigateByPath(path, title);
},
onTapHotRecommend(e: WechatMiniprogram.TouchEvent) {
const path = e.currentTarget.dataset.path as string | undefined;
const title = e.currentTarget.dataset.title as string | undefined;
trackWorksheetClick(path, title, 'home_hot');
navigateByPath(path, title);
},
@@ -310,6 +331,7 @@ Page({
const title =
detail.title ||
(e.currentTarget.dataset.title as string | undefined);
trackWorksheetClick(path, title, 'home_section');
navigateByPath(path, title);
},
@@ -23,8 +23,12 @@ Page({
return;
}
// 统一跳转到 focusDraw 页面
// “识别形状”仍使用独立页面;其余题型统一跳转到 focusDraw 页面
if (functionItem.page) {
if (functionId === 'shape-recognition') {
wx.navigateTo({ url: '/focusPages/shape/shape' });
return;
}
let url = `/focusPages/focusDraw/focusDraw?id=${functionId}`;
if (functionItem.mode) {
url += `&mode=${functionItem.mode}`;
@@ -23,6 +23,6 @@ export const UNRELEASED_PAGE_ENTRIES: UnreleasedPageEntry[] = [
id: 'clock-connect',
title: '时钟连线',
subtitle: '看钟表连对应时间,认识时刻',
path: '/mathPages/clockConnect/clockConnect',
path: '/mathPages/clockReading/clockReading?id=clock-connect',
},
];
+6 -2
View File
@@ -4,6 +4,7 @@ export interface DownloadLogRecord {
_id: string;
userId: string;
worksheetId: string;
exportId?: string;
createdAt: string;
worksheet?: {
_id: string;
@@ -22,13 +23,16 @@ export interface DownloadLogRecord {
/**
* 记录下载日志
*/
export async function addDownloadLog(worksheetId: string): Promise<boolean> {
export async function addDownloadLog(
worksheetId: string,
exportId?: string,
): Promise<boolean> {
if (!worksheetId) return false;
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userDownloadLogs',
data: { action: 'add', worksheetId },
data: { action: 'add', worksheetId, exportId },
});
const result = (res?.result as any) || {};
return result.code === 0;
+20 -9
View File
@@ -6,6 +6,7 @@
import { checkAndSaveImage } from './saveImage';
import tracker from './tracker';
import { addDownloadLog } from './downloadLogs';
import { generateUUID } from './uuid';
import {
getUnlockMethod,
isDownloadLimitEnabled,
@@ -131,23 +132,32 @@ function doDownload(
errorToast?: string;
trackerName?: string;
trackerMode?: string;
trackerPage?: string;
worksheetId?: string;
},
): Promise<boolean> {
if (options.trackerName) {
if (options.trackerMode) {
tracker.reportDownload(options.trackerName, options.trackerMode);
} else {
tracker.reportDownload(options.trackerName);
}
}
const exportId = generateUUID();
const trackerPage = options.trackerPage || options.trackerName || '';
const trackerParams = {
export_id: exportId,
...(options.worksheetId && { worksheet_id: options.worksheetId }),
...(options.trackerName && { worksheet_title: options.trackerName }),
...(options.trackerMode && { mode: options.trackerMode }),
};
return checkAndSaveImage(canvas).then((saved) => {
// 用户点击保存不等于保存成功,二者必须分开统计。
tracker.reportExportClick(trackerPage, trackerParams);
return checkAndSaveImage(canvas).then(async (saved) => {
if (saved) {
// 只在 wx.saveImageToPhotosAlbum 的 success 回调后进入此分支。
recordDownloadSuccess();
tracker.reportExportResult(trackerPage, true, trackerParams);
if (options.worksheetId) {
addDownloadLog(options.worksheetId);
await addDownloadLog(options.worksheetId, exportId);
}
} else {
tracker.reportExportResult(trackerPage, false, trackerParams);
}
return saved;
});
@@ -163,6 +173,7 @@ export async function downloadPrint(
errorToast?: string;
trackerName?: string;
trackerMode?: string;
trackerPage?: string;
worksheetId?: string;
} = {},
): Promise<boolean> {
+14 -3
View File
@@ -1,4 +1,10 @@
import { getUser } from './auth';
import tracker from './tracker';
function getCurrentPageRoute(): string {
const pages = getCurrentPages();
return pages[pages.length - 1]?.route || '';
}
export interface FavoriteRecord {
_id: string;
@@ -31,7 +37,9 @@ export async function addFavorite(worksheetId: string): Promise<boolean> {
data: { action: 'add', worksheetId },
});
const result = (res?.result as any) || {};
return result.code === 0;
const success = result.code === 0;
if (success) tracker.reportFavorite(getCurrentPageRoute(), worksheetId, true);
return success;
} catch (e) {
console.error('addFavorite failed', e);
return false;
@@ -49,7 +57,11 @@ export async function removeFavorite(worksheetId: string): Promise<boolean> {
data: { action: 'remove', worksheetId },
});
const result = (res?.result as any) || {};
return result.code === 0;
const success = result.code === 0;
if (success) {
tracker.reportFavorite(getCurrentPageRoute(), worksheetId, false);
}
return success;
} catch (e) {
console.error('removeFavorite failed', e);
return false;
@@ -133,4 +145,3 @@ export async function batchCheckFavorited(
return {};
}
}
+88 -17
View File
@@ -115,18 +115,96 @@ class Tracker {
};
}
/**
* 统一业务埋点入口。pageName 使用页面 route,内容使用稳定的 worksheet_id。
*/
report(
eventName: string,
pageName: string,
extraParams?: Record<string, any>,
): void {
try {
this.sendEvent(
eventName,
this.getCommonEventParams(eventName, pageName, extraParams),
);
} catch (error) {
console.error(`上报 ${eventName} 事件失败:`, error);
}
}
reportPageView(pageName: string): void {
this.report('page_view', pageName);
}
reportWorksheetClick(
pageName: string,
extraParams: Record<string, any>,
): void {
this.report('worksheet_click', pageName, extraParams);
}
reportPreviewView(
pageName: string,
extraParams: Record<string, any>,
): void {
this.report('preview_view', pageName, extraParams);
}
reportGenerateStart(
pageName: string,
extraParams: Record<string, any>,
): void {
this.report('generate_start', pageName, extraParams);
}
reportGenerateResult(
pageName: string,
success: boolean,
extraParams: Record<string, any>,
): void {
this.report(
success ? 'generate_success' : 'generate_fail',
pageName,
extraParams,
);
}
reportExportClick(
pageName: string,
extraParams: Record<string, any>,
): void {
this.report('export_click', pageName, extraParams);
}
reportExportResult(
pageName: string,
success: boolean,
extraParams: Record<string, any>,
): void {
this.report(
success ? 'export_success' : 'export_fail',
pageName,
extraParams,
);
}
reportFavorite(
pageName: string,
worksheetId: string,
favorited: boolean,
): void {
this.report(favorited ? 'favorite_add' : 'favorite_remove', pageName, {
worksheet_id: worksheetId,
});
}
/**
* 上报分享点击事件
* @param pageName 页面名称
*/
reportShare(pageName: string): void {
try {
const params = this.getCommonEventParams('share_click', pageName);
this.sendEvent('share_click', params);
} catch (error) {
console.error('上报分享事件失败:', error);
}
reportShare(pageName: string, extraParams?: Record<string, any>): void {
this.report('share_click', pageName, extraParams);
}
/**
@@ -135,15 +213,8 @@ class Tracker {
* @param mode 模式(可选)
*/
reportDownload(pageName: string, mode?: string): void {
try {
const params = this.getCommonEventParams('download', pageName, {
...(mode && { mode }),
});
this.sendEvent('download', params);
} catch (error) {
console.error('上报下载事件失败:', error);
}
// 兼容旧调用;新代码请使用 reportExportClick / reportExportResult。
this.reportExportClick(pageName, { ...(mode && { mode }) });
}
/**