From 030afb8213e232ee9925dadc30595fdbbbe74450 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 24 Apr 2026 13:02:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(miniprogram):=20=E5=9F=8B=E7=82=B9?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E8=BF=BD=E8=B8=AA=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 analytics.ts:trackEvent/trackPageView/flushEvents - 事件队列本地缓存,批量上报到 /analytics/batch - 首页 page_view、预约创建、随访提交、健康数据录入四个关键埋点 --- .../src/pages/appointment/create/index.tsx | 2 + .../src/pages/followup/detail/index.tsx | 2 + .../src/pages/health/input/index.tsx | 2 + apps/miniprogram/src/pages/index/index.tsx | 2 + apps/miniprogram/src/services/analytics.ts | 82 +++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 apps/miniprogram/src/services/analytics.ts diff --git a/apps/miniprogram/src/pages/appointment/create/index.tsx b/apps/miniprogram/src/pages/appointment/create/index.tsx index c554924..15ae1aa 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.tsx +++ b/apps/miniprogram/src/pages/appointment/create/index.tsx @@ -4,6 +4,7 @@ import Taro from '@tarojs/taro'; import { listDoctors, createAppointment, calendarView } from '../../../services/appointment'; import { useAuthStore } from '../../../stores/auth'; import { TEMPLATE_IDS } from '@/services/wechat-templates'; +import { trackEvent } from '@/services/analytics'; import StepIndicator from '../../../components/StepIndicator'; import WeekCalendar from '../../../components/WeekCalendar'; import './index.scss'; @@ -112,6 +113,7 @@ export default function AppointmentCreate() { reason: reason.trim() || undefined, }); Taro.showToast({ title: '预约成功', icon: 'success' }); + trackEvent('appointment_create', { doctor_id: selectedDoctor.id, date: appointmentDate }); // 订阅消息引导 const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER; if (tmplId) { diff --git a/apps/miniprogram/src/pages/followup/detail/index.tsx b/apps/miniprogram/src/pages/followup/detail/index.tsx index ec50e54..7998f37 100644 --- a/apps/miniprogram/src/pages/followup/detail/index.tsx +++ b/apps/miniprogram/src/pages/followup/detail/index.tsx @@ -4,6 +4,7 @@ import Taro, { useRouter } from '@tarojs/taro'; import { getTaskDetail, submitRecord } from '../../../services/followup'; import type { FollowUpTask } from '../../../services/followup'; import { TEMPLATE_IDS } from '@/services/wechat-templates'; +import { trackEvent } from '@/services/analytics'; import Loading from '../../../components/Loading'; import ErrorState from '../../../components/ErrorState'; import './index.scss'; @@ -42,6 +43,7 @@ export default function FollowUpDetail() { content: { text: content.trim() }, }); Taro.showToast({ title: '提交成功', icon: 'success' }); + trackEvent('followup_submit', { task_id: id }); const tmplId = TEMPLATE_IDS.FOLLOWUP_REMINDER; if (tmplId) { try { await Taro.requestSubscribeMessage({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ } diff --git a/apps/miniprogram/src/pages/health/input/index.tsx b/apps/miniprogram/src/pages/health/input/index.tsx index 67c8938..bbd0637 100644 --- a/apps/miniprogram/src/pages/health/input/index.tsx +++ b/apps/miniprogram/src/pages/health/input/index.tsx @@ -5,6 +5,7 @@ import { z } from 'zod'; import { inputVitalSign } from '../../../services/health'; import { useAuthStore } from '../../../stores/auth'; import { useHealthStore } from '@/stores/health'; +import { trackEvent } from '@/services/analytics'; import './index.scss'; const INDICATORS = [ @@ -76,6 +77,7 @@ export default function HealthInput() { }); clearCache(); Taro.showToast({ title: '录入成功', icon: 'success' }); + trackEvent('health_data_input', { type: indicatorType }); setTimeout(() => Taro.navigateBack(), 1000); } catch (e: unknown) { const msg = e instanceof Error ? e.message : '录入失败'; diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index 7bdfe4c..86a5797 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -4,6 +4,7 @@ import { useAuthStore } from '../../stores/auth'; import { useHealthStore } from '../../stores/health'; import EmptyState from '../../components/EmptyState'; import Loading from '../../components/Loading'; +import { trackPageView } from '@/services/analytics'; import './index.scss'; export default function Index() { @@ -13,6 +14,7 @@ export default function Index() { useDidShow(() => { restoreAuth(); refreshToday(); + trackPageView('home'); }); const hour = new Date().getHours(); diff --git a/apps/miniprogram/src/services/analytics.ts b/apps/miniprogram/src/services/analytics.ts new file mode 100644 index 0000000..a31a4d5 --- /dev/null +++ b/apps/miniprogram/src/services/analytics.ts @@ -0,0 +1,82 @@ +import Taro from '@tarojs/taro'; + +type EventName = + | 'page_view' + | 'login' + | 'bind_phone' + | 'health_data_input' + | 'health_trend_view' + | 'appointment_create' + | 'appointment_detail' + | 'followup_submit' + | 'report_view' + | 'article_view' + | 'article_share' + | 'medication_add' + | 'family_add' + | 'profile_edit'; + +interface AnalyticsEvent { + event: EventName | string; + properties?: Record; + timestamp: number; + userId?: string; + patientId?: string; +} + +const QUEUE_KEY = 'analytics_queue'; +const MAX_QUEUE_SIZE = 50; + +function getQueue(): AnalyticsEvent[] { + return Taro.getStorageSync(QUEUE_KEY) || []; +} + +function setQueue(queue: AnalyticsEvent[]): void { + Taro.setStorageSync(QUEUE_KEY, queue.slice(-MAX_QUEUE_SIZE)); +} + +export function trackEvent(event: EventName | string, properties?: Record): void { + const userId = Taro.getStorageSync('user')?.id; + const patientId = Taro.getStorageSync('current_patient_id'); + + const evt: AnalyticsEvent = { + event, + properties, + timestamp: Date.now(), + userId, + patientId, + }; + + const queue = getQueue(); + queue.push(evt); + setQueue(queue); +} + +export function trackPageView(pageName: string, properties?: Record): void { + trackEvent('page_view', { page: pageName, ...properties }); +} + +export async function flushEvents(): Promise { + const queue = getQueue(); + if (queue.length === 0) return; + + const batch = queue.slice(); + setQueue([]); + + try { + const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'; + await Taro.request({ + url: `${BASE_URL}/analytics/batch`, + method: 'POST', + data: { events: batch }, + }); + } catch { + // 发送失败,回填队列 + const current = getQueue(); + setQueue([...batch.slice(-MAX_QUEUE_SIZE + current.length), ...current]); + } +} + +export function getQueueSize(): number { + return getQueue().length; +}