zlwu 7 mēneši atpakaļ
vecāks
revīzija
ebd3df966e

+ 52 - 7
frontend/src/pages/LumiOutfitsPage.tsx

@@ -1111,7 +1111,12 @@ export const LumiOutfitsPage = () => {
                       <button
                         key={filter.key}
                         type="button"
-                        onClick={() => setFurnitureFilter(filter.key)}
+                        onClick={wrapTrackedClick(
+                          'lumi_outfits',
+                          'furniture_filter',
+                          () => setFurnitureFilter(filter.key),
+                          { properties: { filter_key: filter.key } },
+                        )}
                         aria-label={filter.ariaLabel}
                         style={{
                           width: px2vmin(44),
@@ -1165,7 +1170,19 @@ export const LumiOutfitsPage = () => {
                     <button
                       key={item.item_id}
                       type="button"
-                      onClick={() => handleFurnitureCardClick(item)}
+                      onClick={wrapTrackedClick(
+                        'lumi_outfits',
+                        'owned_item_click',
+                        () => handleFurnitureCardClick(item),
+                        {
+                          properties: {
+                            item_id: item.item_id,
+                            biz_type: item.biz_type,
+                            item_type: item.item_type,
+                            is_in_use: item.is_in_use,
+                          },
+                        },
+                      )}
                       style={{
                         width: px2vmin(90),
                         height: px2vmin(90),
@@ -1303,7 +1320,17 @@ export const LumiOutfitsPage = () => {
             backgroundColor: 'transparent',
             zIndex: 10030,
           }}
-          onClick={() => setActiveFurnitureModalItem(null)}
+          onClick={wrapTrackedClick(
+            'lumi_outfits',
+            'furniture_modal_close',
+            () => setActiveFurnitureModalItem(null),
+            {
+              properties: {
+                item_id: activeFurnitureModalItem.item_id,
+                modal_mode: furnitureModalMode,
+              },
+            },
+          )}
         >
           <div
             onClick={(e) => e.stopPropagation()}
@@ -1420,9 +1447,17 @@ export const LumiOutfitsPage = () => {
             >
               <button
                 type="button"
-                onClick={() => {
-                  handleFurnitureModalConfirm();
-                }}
+                onClick={wrapTrackedClick(
+                  'lumi_outfits',
+                  'furniture_modal_confirm',
+                  () => handleFurnitureModalConfirm(),
+                  {
+                    properties: {
+                      item_id: activeFurnitureModalItem.item_id,
+                      modal_mode: furnitureModalMode,
+                    },
+                  },
+                )}
                 disabled={furnitureSubmitting}
                 style={{
                   width: px2vmin(146),
@@ -1448,7 +1483,17 @@ export const LumiOutfitsPage = () => {
               </button>
               <button
                 type="button"
-                onClick={() => setActiveFurnitureModalItem(null)}
+                onClick={wrapTrackedClick(
+                  'lumi_outfits',
+                  'furniture_modal_cancel',
+                  () => setActiveFurnitureModalItem(null),
+                  {
+                    properties: {
+                      item_id: activeFurnitureModalItem.item_id,
+                      modal_mode: furnitureModalMode,
+                    },
+                  },
+                )}
                 disabled={furnitureSubmitting}
                 style={{
                   width: px2vmin(146),

+ 95 - 19
frontend/src/pages/LumiShopPage.tsx

@@ -420,6 +420,29 @@ export const LumiShopPage = () => {
       } catch {
         /* ignore cart refresh */
       }
+
+      // 单独购买成功后:自动装配到当前幕布(购物车购买不自动装配)
+      try {
+        const isBackdrop = itemToPurchase.item_type === 'backdrop' || itemToPurchase.biz_type === 'wallpaper';
+        if (isBackdrop) {
+          await updateChatbotBackdrop(userId, itemToPurchase.item_id);
+        } else {
+          await equipChatbotFurniture(userId, itemToPurchase.item_id);
+        }
+        await refreshChatbotLayout();
+        await refreshFurnitureCatalog();
+      } catch (equipError: unknown) {
+        if (equipError instanceof ApiError) {
+          if (equipError.code === 'SLOT_DEPENDENCY_NOT_SATISFIED') {
+            showFurnitureNotice(equipError.backendMessage || tProfile('lumiFurnitureDependencyNotMetNotice'));
+          } else {
+            showFurnitureNotice(equipError.backendMessage || tError(equipError.code));
+          }
+        } else {
+          showFurnitureNotice(tError('SERVER_ERROR'));
+        }
+      }
+
       setActivePurchaseItem(null);
       setPurchaseSuccessItem(itemToPurchase);
     } catch (error: unknown) {
@@ -431,7 +454,15 @@ export const LumiShopPage = () => {
     } finally {
       setPurchaseSubmitting(false);
     }
-  }, [activePurchaseItem, purchaseSubmitting, refreshFurnitureCatalog, showFurnitureNotice, tError]);
+  }, [
+    activePurchaseItem,
+    purchaseSubmitting,
+    refreshChatbotLayout,
+    refreshFurnitureCatalog,
+    showFurnitureNotice,
+    tError,
+    tProfile,
+  ]);
 
   const onPurchaseBuyClick = useMemo(
     () => wrapTrackedClick('lumi_shop', 'purchase_buy', handlePurchaseBuy),
@@ -1286,7 +1317,12 @@ export const LumiShopPage = () => {
                       <button
                         key={filter.key}
                         type="button"
-                        onClick={() => setFurnitureFilter(filter.key)}
+                        onClick={wrapTrackedClick(
+                          'lumi_shop',
+                          'furniture_filter',
+                          () => setFurnitureFilter(filter.key),
+                          { properties: { filter_key: filter.key } },
+                        )}
                         aria-label={filter.ariaLabel}
                         style={{
                           width: px2vmin(44),
@@ -1341,7 +1377,19 @@ export const LumiShopPage = () => {
                     <button
                       key={item.item_id}
                       type="button"
-                      onClick={() => handleFurnitureCardClick(item)}
+                      onClick={wrapTrackedClick(
+                        'lumi_shop',
+                        'catalog_item_click',
+                        () => handleFurnitureCardClick(item),
+                        {
+                          properties: {
+                            item_id: item.item_id,
+                            biz_type: item.biz_type,
+                            item_type: item.item_type,
+                            is_owned: item.is_owned,
+                          },
+                        },
+                      )}
                       style={{
                         width: px2vmin(90),
                         height: px2vmin(102),
@@ -1453,7 +1501,7 @@ export const LumiShopPage = () => {
             backgroundColor: 'transparent',
             zIndex: 10029,
           }}
-          onClick={closeShopCartModal}
+          onClick={wrapTrackedClick('lumi_shop', 'shop_cart_close', closeShopCartModal)}
         >
           <div
             onClick={(e) => e.stopPropagation()}
@@ -1540,10 +1588,20 @@ export const LumiShopPage = () => {
                 >
                   <button
                     type="button"
-                    onClick={(e) => {
-                      e.stopPropagation();
-                      void handleRemoveShopCartItem(line.catalogItem.item_id);
-                    }}
+                    onClick={wrapTrackedClick(
+                      'lumi_shop',
+                      'shop_cart_remove_item',
+                      (e) => {
+                        e?.stopPropagation();
+                        void handleRemoveShopCartItem(line.catalogItem.item_id);
+                      },
+                      {
+                        properties: {
+                          item_id: line.catalogItem.item_id,
+                          quantity: line.quantity,
+                        },
+                      },
+                    )}
                     disabled={shopCartRemovingItemIds[line.catalogItem.item_id]}
                     style={{
                       position: 'absolute',
@@ -1643,7 +1701,7 @@ export const LumiShopPage = () => {
             >
               <button
                 type="button"
-                onClick={closeShopCartModal}
+                onClick={wrapTrackedClick('lumi_shop', 'shop_cart_close', closeShopCartModal)}
                 style={{
                   width: px2vmin(146),
                   height: px2vmin(56),
@@ -1671,9 +1729,14 @@ export const LumiShopPage = () => {
 
               <button
                 type="button"
-                onClick={() => {
-                  void handleShopCartPurchaseAll();
-                }}
+                onClick={wrapTrackedClick(
+                  'lumi_shop',
+                  'shop_cart_purchase_all',
+                  () => {
+                    void handleShopCartPurchaseAll();
+                  },
+                  { properties: { cart_total_quantity: shopCartTotalQty } },
+                )}
                 disabled={shopCartPurchasing || shopCartItems.length <= 0}
                 style={{
                   width: px2vmin(146),
@@ -1735,7 +1798,7 @@ export const LumiShopPage = () => {
             backgroundColor: 'transparent',
             zIndex: 10031,
           }}
-          onClick={() => setActivePurchaseItem(null)}
+          onClick={wrapTrackedClick('lumi_shop', 'purchase_modal_close', () => setActivePurchaseItem(null))}
         >
           <div
             onClick={(e) => e.stopPropagation()}
@@ -2002,7 +2065,7 @@ export const LumiShopPage = () => {
             backgroundColor: 'rgba(0, 0, 0, 0.4)',
             zIndex: 10032,
           }}
-          onClick={() => setPurchaseSuccessItem(null)}
+          onClick={wrapTrackedClick('lumi_shop', 'purchase_success_close', () => setPurchaseSuccessItem(null))}
         >
           {/* 庆祝 GIF:位于底部弹窗上方 5px */}
           <div
@@ -2215,7 +2278,12 @@ export const LumiShopPage = () => {
             backgroundColor: 'transparent',
             zIndex: 10030,
           }}
-          onClick={() => setActiveFurnitureModalItem(null)}
+          onClick={wrapTrackedClick('lumi_shop', 'furniture_modal_close', () => setActiveFurnitureModalItem(null), {
+            properties: {
+              item_id: activeFurnitureModalItem.item_id,
+              modal_mode: furnitureModalMode,
+            },
+          })}
         >
           <div
             onClick={(e) => e.stopPropagation()}
@@ -2332,9 +2400,12 @@ export const LumiShopPage = () => {
             >
               <button
                 type="button"
-                onClick={() => {
-                  handleFurnitureModalConfirm();
-                }}
+                onClick={wrapTrackedClick('lumi_shop', 'furniture_modal_confirm', () => handleFurnitureModalConfirm(), {
+                  properties: {
+                    item_id: activeFurnitureModalItem.item_id,
+                    modal_mode: furnitureModalMode,
+                  },
+                })}
                 disabled={furnitureSubmitting}
                 style={{
                   width: px2vmin(146),
@@ -2360,7 +2431,12 @@ export const LumiShopPage = () => {
               </button>
               <button
                 type="button"
-                onClick={() => setActiveFurnitureModalItem(null)}
+                onClick={wrapTrackedClick('lumi_shop', 'furniture_modal_cancel', () => setActiveFurnitureModalItem(null), {
+                  properties: {
+                    item_id: activeFurnitureModalItem.item_id,
+                    modal_mode: furnitureModalMode,
+                  },
+                })}
                 disabled={furnitureSubmitting}
                 style={{
                   width: px2vmin(146),

+ 2 - 0
frontend/src/utils/tracking.ts

@@ -5,7 +5,9 @@ import { getUserId } from '@/utils/storage';
 
 // 路由 path → page_id 映射,需与 .cursorrules 中的命名约定保持一致
 const PATH_TO_PAGE_ID: Record<string, string> = {
+  '/': 'root_redirect',
   '/welcome': 'welcome',
+  '/login': 'login',
   '/begin': 'begin',
   '/login-register': 'login_register',
   '/loading': 'loading',

+ 64 - 0
项目文档/前端埋点缺口分析.md

@@ -0,0 +1,64 @@
+# 前端埋点缺口分析(frontend)
+
+## 埋点机制(当前实现)
+- 页面进入:`frontend/src/components/PageVisitReporter.tsx` -> `trackPageEnter(path)` -> `event_name=page.${pageId}__enter`
+- 按钮点击:`wrapTrackedClick(pageId, buttonId, onClick)` -> `trackButtonClick()` -> `event_name=button.${pageId}.${buttonId}__click`
+
+### 注意事项(上报条件)
+- `trackPageEnter`:当 `getUserId()` 为空时不会上报 `page.*__enter`
+- `trackButtonClick`:当 `getUserId()` 为空时不会上报 `button.*__click`
+
+## 缺口汇总
+
+### A. 页面进入埋点缺失(page.${pageId}__enter)
+`frontend/src/utils/tracking.ts` 的 `PATH_TO_PAGE_ID` 未覆盖以下路由,因此这些路径进入时不会上报 `page.*__enter`:
+- `/`(`RootRedirect` 展示加载态但未映射)
+- `/login`(路由仅做跳转到 `/email-login`)
+
+已修复:已在 `frontend/src/utils/tracking.ts` 增加 `/` 与 `/login` 的 `PATH_TO_PAGE_ID` 映射。
+
+### B. 点击类埋点缺失(button.*__click)
+以下页面内存在明显可点击操作,但代码里未使用 `wrapTrackedClick()`/`trackButtonClick()` 进行上报(或是通过非埋点方式直接改 state):
+
+#### 1) `lumi_shop`(`frontend/src/pages/LumiShopPage.tsx`)
+- 商品过滤按钮:`setFurnitureFilter(filter.key)` 的点击未埋点
+- 商品卡片点击:`handleFurnitureCardClick(item)` 的点击未埋点
+- 购物车弹窗内“移除”按钮:`handleRemoveShopCartItem(line.catalogItem.item_id)` 未埋点
+- 购物车弹窗内“关闭/继续浏览”:`closeShopCartModal` 未埋点
+- 购物车弹窗内“全部购买”:`handleShopCartPurchaseAll()` 未埋点
+- 购买商品弹窗遮罩/关闭:`setActivePurchaseItem(null)` 未埋点
+- 家具装备/移除弹窗:
+  - 确认按钮:`handleFurnitureModalConfirm()` 未埋点
+  - 取消按钮:`setActiveFurnitureModalItem(null)` 未埋点
+  - 遮罩关闭:`setActiveFurnitureModalItem(null)` 未埋点
+- 购买成功弹窗遮罩/关闭:`setPurchaseSuccessItem(null)` 未埋点
+
+已修复:已在 `frontend/src/pages/LumiShopPage.tsx` 将上述交互补齐为 `wrapTrackedClick('lumi_shop', ...)`。
+
+#### 2) `lumi_outfits`(`frontend/src/pages/LumiOutfitsPage.tsx`)
+- 商品过滤按钮:`setFurnitureFilter(filter.key)` 未埋点
+- 商品卡片点击:`handleFurnitureCardClick(item)` 未埋点
+- 服装“帽子卡片”切换:`onCapCardClick` 已埋点(`button.lumi_outfits.cap_outfit_card__click`)
+- 家具装备/移除弹窗:
+  - 确认按钮:`handleFurnitureModalConfirm()` 未埋点
+  - 取消按钮:`setActiveFurnitureModalItem(null)` 未埋点
+  - 遮罩关闭:`setActiveFurnitureModalItem(null)` 未埋点
+
+已修复:已在 `frontend/src/pages/LumiOutfitsPage.tsx` 将剩余未埋点项补齐为 `wrapTrackedClick('lumi_outfits', ...)`。
+
+## 已确认:其余主要页面/步骤的覆盖情况
+- 新手教程页(`/tutorial-1` ~ `/tutorial-10`)不在各步页面直接写 `wrapTrackedClick()`,而是由 `frontend/src/components/ChatBotStaticShell.tsx` 统一通过 `wrapTrackedClick(pageId, ...)` 上报。
+- 其它路由页面(欢迎/登录/验证/个人中心/ChatBot 等)主交互按钮大多已使用 `wrapTrackedClick()`。
+- `UserAgreementModal` / `PrivacyPolicyModal` / `MissionEnergyStoneModal` / `IntimacyModal` 在传入 `trackingPageId` 时,遮罩关闭/主要按钮是会走 `wrapTrackedClick()` 的。
+
+## 建议修复方向(不影响当前结论)
+- 对上述未埋点的交互点,补充 `wrapTrackedClick(pageId, buttonId, handler)`:
+  - `lumi_shop`:建议新增 buttonId,例如 `furniture_filter`、`catalog_item_click`、`shop_cart_remove_item`、`shop_cart_close`、`shop_cart_purchase_all`、`purchase_modal_close`、`furniture_modal_confirm`、`furniture_modal_cancel`、`purchase_success_close` 等,并在 `options.properties` 中附带 `filter_key` / `item_id` / `modal_mode`。
+  - `lumi_outfits`:建议新增 buttonId,例如 `furniture_filter`、`owned_item_click`、`cap_card_toggle`、`furniture_modal_confirm`、`furniture_modal_cancel`、`outfits_modal_close` 等。
+
+## 本次核对结论(当前代码)
+- 入口页面访问埋点:`frontend/src/components/PageVisitReporter.tsx` 会在所有路由子页面进入时调用 `trackPageEnter(location.pathname)`,且 `frontend/src/utils/tracking.ts` 的 `PATH_TO_PAGE_ID` 已覆盖 `frontend/src/router/index.tsx` 中的所有路由 path(包含 `/` -> `root_redirect`、`/login` -> `login`)。
+- 关键按钮点击埋点:`frontend/src/pages/LumiShopPage.tsx` 与 `frontend/src/pages/LumiOutfitsPage.tsx` 中,文档列出的主要交互点(过滤按钮点击、卡片点击、购物车弹窗移除/关闭/全部购买、购买/移除弹窗遮罩关闭与确认/取消等)已在代码里使用 `wrapTrackedClick(pageId, ...)` 补齐。
+
+因此:未发现需要新增到《前端埋点缺口分析》中的未完成项。
+