Переглянути джерело

feat(log): 添加页面埋点功能

panqiuyao 3 місяців тому
батько
коміт
c7b2a58ad7

+ 121 - 0
frontend/src/apis/log.ts

@@ -0,0 +1,121 @@
+import { GET, POST } from "@/utils/http";
+import { useUuidStore } from "@/stores/modules/uuid";
+import pinia from "@/stores/index";
+
+// 定义埋点参数的类型
+interface LogParams {
+  type?: number;
+  channel?: string;
+  uuid?: string;
+  page?: string;
+  page_url?: string;
+  describe?: any;
+  time?: number;
+  [key: string]: any;
+}
+
+// 定义UUID响应的类型
+interface UuidResponse {
+  code: number;
+  data: {
+    uuid: string;
+  };
+  message: string;
+}
+
+// 获取UUID store实例
+const getUuidStore = () => {
+  return useUuidStore(pinia);
+};
+
+// 公共参数
+const getPubParams = () => {
+  const uuidStore = getUuidStore();
+  return {
+    channel: 'aigc-camera',
+    uuid: uuidStore.getUuid || ''
+  };
+};
+
+/**
+ * 获取UUID
+ */
+export async function getUUid(): Promise<UuidResponse> {
+  return GET('/api/uuid', {}, {
+    loading: false,
+    showErrorMessage: false
+  });
+}
+
+/**
+ * 设置UUID(用于与store集成)
+ * @param uuid UUID字符串
+ */
+export function setUuid(uuid: string): void {
+  const uuidStore = getUuidStore();
+  uuidStore.setUuid(uuid);
+}
+
+/**
+ * 获取当前UUID
+ * @returns 当前UUID或null
+ */
+export function getCurrentUuid(): string | null {
+  const uuidStore = getUuidStore();
+  return uuidStore.getUuid;
+}
+
+/**
+ * 埋点接口
+ * @param params 埋点参数
+ */
+export async function setLog(params: LogParams): Promise<void> {
+  console.log('setLog 被调用,参数:', params); // 调试信息
+  
+  const uuidStore = getUuidStore();
+  
+  // 埋点函数
+  const setLogFun = async (logParams: LogParams) => {
+    const pubParams = getPubParams();
+    const requestData = {
+      type: 1,
+      ...pubParams,
+      uuid: uuidStore.getUuid || '',
+      ...logParams
+    };
+    
+    console.log('发送埋点请求,数据:', requestData); // 调试信息
+    
+    try {
+      await POST('/api/record/point', requestData, {
+        loading: false,
+        showErrorMessage: false
+      });
+      console.log('埋点请求成功'); // 调试信息
+    } catch (error) {
+      console.error('埋点请求失败:', error); // 调试信息
+    }
+  };
+
+  // 检查UUID是否存在
+  if (uuidStore.hasUuid) {
+    console.log('使用缓存的UUID:', uuidStore.getUuid); // 调试信息
+    await setLogFun(params);
+  } else {
+    console.log('UUID不存在,开始获取UUID'); // 调试信息
+    // UUID不存在,先获取UUID
+    try {
+      const res = await getUUid();
+      console.log('获取UUID响应:', res); // 调试信息
+      if (res.code === 0 && res.data?.uuid) {
+        uuidStore.setUuid(res.data.uuid);
+        console.log('设置UUID缓存:', uuidStore.getUuid); // 调试信息
+        await setLogFun(params);
+      } else {
+        console.error('获取UUID失败:', res.message);
+      }
+    } catch (error) {
+      console.error('获取UUID异常:', error);
+    }
+  }
+}

+ 6 - 0
frontend/src/main.ts

@@ -6,6 +6,7 @@ import App from './App.vue'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
 import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import { lissenLog, log } from './utils/log'
 
 const app = createApp(App)
 app.use(ElementPlus)
@@ -14,4 +15,9 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 }
 app.use(pinia)
 app.use(router)
+
+// 注册埋点指令和路由监听 - 确保在router使用后立即注册
+lissenLog(app)
+log(router)
+
 app.mount('#app')

+ 1 - 0
frontend/src/router/index.ts

@@ -16,6 +16,7 @@ const routes: RouteRecordRaw[] = [
         name: "home",
         component: () => import("@/views/Home/index.vue"),
         meta: {
+            title: '首页',
             noAuth: true,
         },
     },

+ 32 - 0
frontend/src/stores/modules/uuid.ts

@@ -0,0 +1,32 @@
+import { defineStore } from 'pinia'
+
+export const useUuidStore = defineStore('uuid', {
+  state: () => ({
+    uuid: null as string | null
+  }),
+  
+  getters: {
+    getUuid: (state) => state.uuid,
+    hasUuid: (state) => !!state.uuid
+  },
+  
+  actions: {
+    setUuid(uuid: string) {
+      this.uuid = uuid
+    },
+    
+    clearUuid() {
+      this.uuid = null
+    }
+  },
+  
+  persist: {
+    enabled: true,
+    strategies: [
+      {
+        key: 'uuid-store',
+        storage: localStorage
+      }
+    ]
+  }
+}) 

+ 3 - 2
frontend/src/utils/http.ts

@@ -184,10 +184,11 @@ service.interceptors.response.use(
  *
  * @template T 泛型,表示返回数据的类型
  * @param {string} url 请求的 URL
- * @param {any} [params] 请求参数
+ * @param {any} [data] 请求参数
+ * @param {any} [config] 请求配置
  * @returns {Promise<T>} 返回一个 Promise,解析为响应数据
  */
-export function GET<T>(url: string, data?: any,config): Promise<T> {
+export function GET<T>(url: string, data?: any, config?: any): Promise<T> {
     return service.get(url, {
         params: data,
         loading: config?.loading ?? false,

+ 183 - 0
frontend/src/utils/log.ts

@@ -0,0 +1,183 @@
+import { App } from 'vue'
+import router from '@/router/index'
+import { setLog } from '@/apis/log'
+import tokenInfo from '@/stores/modules/token'
+
+let outTime = 0;
+let outTimeObj: NodeJS.Timeout | null = null;
+
+const outFun = function(status = 'init', currentRouter: any) {
+  if(status == 'init'){
+    outTime = 0;
+    if(outTimeObj) clearInterval(outTimeObj)
+  }
+  outTimeObj = setInterval(() => {
+    if(outTime == 10){
+      //  setLogInfoRemain(currentRouter)
+      outTime = 0
+    } else {
+      outTime++;
+    }
+  }, 1000)
+}
+
+export function setLogInfo(router: any, describe: any) {
+  setLog({
+    page: router.meta.title,
+    page_url: router.fullPath,
+    describe: describe
+  });
+
+  outFun('init', router);
+}
+
+export function setLogInfoHide(router: any, describe: any) {
+  setLog({
+    page: router.meta.title,
+    page_url: router.fullPath,
+    describe: describe,
+    time: outTime,
+    type: 3
+  });
+}
+
+// 停留
+export function setLogInfoRemain(router: any) {
+  setLog({
+    page: router.meta.title,
+    page_url: router.fullPath,
+    describe: {
+      action: '停留' + router.meta.title + '10s',
+      query: { ...router.query, ...router.params }
+    },
+    time: outTime,
+    type: 6
+  });
+}
+
+/*
+* 埋点--操作
+*/
+export function lissenLog(app: App) {
+  function log(el: HTMLElement, binding: any, thisRoute = router.currentRoute.value) {
+    return function(e: Event) {
+      e.stopPropagation();
+      let log = binding.value;
+      if (!log) {
+        try {
+          log = JSON.parse(el.getAttribute('log') || '{}');
+        } catch (e) {
+          log = {};
+        }
+      }
+
+      setLog({
+        page: thisRoute.meta.title,
+        page_url: thisRoute.fullPath,
+        describe: log.describe,
+        type: 5,
+      });
+    }
+  }
+
+  /*
+  * 获取具体参数,传参数到后台,
+  * 因为vue3.0中用了vue2.0的写法,在v-log中,数据无法响应式,故部分log信息 会绑定到dom的log下
+  */
+  /* 绑定 v-log 事件 默认为click */
+  app.directive('log', {
+    mounted(el: HTMLElement, binding: any) {
+      el.addEventListener('click', log(el, binding));
+    },
+    unmounted(el: HTMLElement, binding: any) {
+      el.removeEventListener('click', log(el, binding));
+    }
+  });
+}
+
+/*埋点 点击*/
+export async function clickLog(describe: any, route?: any, type = 5) {
+  const currentRoute = router.currentRoute.value;
+  const page = route?.meta?.title || currentRoute?.meta?.title;
+  const page_url = route?.path || currentRoute?.fullPath;
+
+  await setLog({
+    page,
+    page_url,
+    describe: describe,
+    type,
+  });
+}
+
+/*
+* 埋点--进入页面
+*/
+export function log(router: any) {
+  console.log('注册路由监听器...'); // 调试信息
+
+  // 确保router对象存在
+  if (!router) {
+    console.error('Router对象不存在');
+    return;
+  }
+
+  // 检查router.afterEach方法是否存在
+  if (typeof router.afterEach !== 'function') {
+    console.error('router.afterEach方法不存在');
+    return;
+  }
+
+  router.afterEach((to: any, from: any) => {
+    console.log('路由变化:', {
+      from: from?.path || 'null',
+      to: to?.path || 'null',
+      toMeta: to?.meta,
+      fromMeta: from?.meta
+    }); // 调试信息
+
+    /*
+    * 第一次 进入页面
+    */
+    const tokenStore = tokenInfo();
+    const hasToken = tokenStore.getToken;
+
+    let this_to = {
+      ...to,
+      ...{},
+    };
+    let this_from = {
+      ...from,
+      ...{},
+    };
+
+    // 离开页面埋点 - 排除重定向情况
+    if(from && from.path !== '/' && from.meta?.log !== false && from.meta?.title){
+      console.log('离开页面埋点:', from.meta.title); // 调试信息
+      setLogInfoHide(this_from, {
+        action: '离开' + this_from.meta.title,
+        query: { ...from.query, ...from.params }
+      });
+    }
+
+    // 进入页面埋点 - 排除根路径和重定向
+    if (to.path !== '/' && to.meta?.log !== false && to.meta?.title) {
+      console.log('进入页面埋点:', to.meta.title); // 调试信息
+      setLogInfo(this_to, {
+        action: '进入' + this_to.meta.title,
+        query: { ...to.query, ...to.params }
+      });
+    }
+  });
+
+  console.log('路由监听器注册完成'); // 调试信息
+}
+
+// 测试函数 - 用于验证路由监听是否工作
+export function testLogFunction() {
+  console.log('测试埋点函数被调用');
+  setLog({
+    page: '测试页面',
+    page_url: '/test',
+    describe: { action: '测试埋点' }
+  });
+}

+ 1 - 1
frontend/src/views/OTA/index.vue

@@ -168,7 +168,7 @@ onMounted(() => {
             <el-table-column label="描述">
               <template #default="{ row }">
                 <el-tooltip :content="row.describe" placement="top" :show-when="hover" :width="500">
-                  <span class="version-describe">{{ row.describe }}</span>
+                  <span class="version-describe" v-html="row.describe"></span>
                 </el-tooltip>
               </template>
             </el-table-column>

+ 26 - 4
frontend/src/views/Photography/detail.vue

@@ -105,7 +105,7 @@
 
         <div class="template-list">
           <div v-for="(template, index) in visibleTemplates" :key="index" class="template-item"
-            @click="form.selectTemplate = template">
+            @click="form.selectTemplate = template" v-log="{ describe: { action: '选择详情模板', template_name: template.template_name } }">
             <el-image :src="template.template_cover_image" fit="contain" class="cur-p"
               style="width: 100%; display: block;" />
             <div class="select-warp" :class="form.selectTemplate.id == template.id ? 'active' : ''">
@@ -115,7 +115,7 @@
             </div>
             <div class="template-info">
               <span class="mar-left-10 chaochu_1">{{ template.template_name }}</span>
-              <div class="template-view" @click="viewTemplate(template)">查看</div>
+              <div class="template-view" @click="viewTemplate(template)" v-log="{ describe: { action: '查看模板详情', template_name: template.template_name } }">查看</div>
             </div>
           </div>
         </div>
@@ -171,7 +171,7 @@
             <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
             详情资料准备 (2选1)
 
-            <el-button v-if="form.dataType == '1'" type="text" class="mar-left-10 fs-16"  @click="downloadExcel">下载商品基础资料模版</el-button>
+            <el-button v-if="form.dataType == '1'" type="text" class="mar-left-10 fs-16"  @click="downloadExcel" v-log="{ describe: { action: '下载Excel模板' } }">下载商品基础资料模版</el-button>
           </div>
         </div>
 
@@ -187,7 +187,7 @@
             <div class="flex bottom mar-left-20" style="flex-grow: 1;">
               <el-input type="textarea" v-model="form.excel_path" />
             </div>
-            <el-button class="select-button button--primary1  mar-left-20" type="primary" @click="selectExcel">
+            <el-button class="select-button button--primary1  mar-left-20" type="primary" @click="selectExcel" v-log="{ describe: { action: '选择Excel文件' } }">
               <img src="@/assets/images/Photography/wenjian.png" style="width: 16px; margin-right: 4px;" />
               选择</el-button>
           </div>
@@ -227,6 +227,7 @@ import { getCompanyTemplatesApi } from '@/apis/other'
 import tokenInfo from '@/stores/modules/token';
 import useUserInfo from "@/stores/modules/user";
 import { useRoute, useRouter } from 'vue-router'
+import { clickLog, setLogInfo } from '@/utils/log'
 
 
 import { ElMessage, ElMessageBox } from 'element-plus'
@@ -302,6 +303,8 @@ const form = reactive({
   is_only_cutout:0, //是否仅抠图模式
 })
 onMounted(() => {
+  // 页面访问埋点
+
   const goods_art_data = route.query.goods_art_nos
   goods_art_nos.value = Array.isArray(goods_art_data) ? goods_art_data : [goods_art_data]
   getCompanyTemplates()
@@ -359,6 +362,18 @@ const onSizeChange = (data) => {
 
 // 开始生成操作
 const generate = async function () {
+  // 埋点:开始生成详情页
+  clickLog({
+    describe: {
+      action: '开始生成详情页',
+      is_only_cutout: form.is_only_cutout,
+      dataType: form.dataType,
+      template_name: form.selectTemplate.template_name,
+      goods_count: goods_art_nos.value.length,
+      goods_art_nos: goods_art_nos.value
+    }
+  }, route);
+
   if(form.is_only_cutout == 0 ){
 
     if ( form.dataType == '1'  && !form.excel_path) {
@@ -438,6 +453,9 @@ const generate = async function () {
 
     // 全部生成成功
     function handleSuccess(href, loadingMsg) {
+      // 埋点:生成完成
+      setLogInfo(route, { action: '生成完成', output_folder: href, message: loadingMsg });
+
       completeDirectory.value = href
       progress.value = 100
       disabledButton.value = false
@@ -457,6 +475,10 @@ const generate = async function () {
     }
     // 全部生成失败
     function handleFailure(partSuccessList) {
+      // 埋点:生成失败(携带货号)
+      const failedGoods = (partSuccessList || []).map(item => item.goods_art_no).filter(Boolean)
+      setLogInfo(route, { action: '生成失败', error_count: partSuccessList.length, goods_art_nos: goods_art_nos.value, failed_goods_art_nos: failedGoods });
+
       let errorList = []
       partSuccessList.map(item => {
         if (!item.success) {

+ 18 - 7
frontend/src/views/Photography/shot.vue

@@ -85,8 +85,8 @@
           <span class="history-title flex between">
             <div>拍摄记录</div>
             <div class="c-666 fs-12" v-if="goodsList.length" >
-                    <el-button :disabled="!(runLoading || takePictureLoading)" @click="oneClickStop" v-if="configInfoStore.appModel === 1" class="input-button" type="primary" size="mini">一键停止</el-button>
-                    <el-button :disabled="runLoading || takePictureLoading" @click="delAll" class="input-button" type="danger" size="mini">一键删除</el-button>
+                    <el-button :disabled="!(runLoading || takePictureLoading)" @click="oneClickStop" v-if="configInfoStore.appModel === 1" class="input-button" type="primary" size="mini" v-log="{ describe: { action: '一键停止拍摄' } }">一键停止</el-button>
+                    <el-button :disabled="runLoading || takePictureLoading" @click="delAll" class="input-button" type="danger" size="mini" v-log="{ describe: { action: '一键删除所有记录' } }">一键删除</el-button>
             </div>
           </span>
           <img class="divider-line" referrerpolicy="no-referrer" src="@/assets/images/Photography/divider-line.png" />
@@ -116,12 +116,12 @@
                       </template>
                     </el-dropdown>
 
-                    <el-button size="small" :disabled="runLoading || takePictureLoading" @click="delGoods({goods_art_nos:[item.goods_art_no]})">删除</el-button>
+                    <el-button size="small" :disabled="runLoading || takePictureLoading" @click="delGoods({goods_art_nos:[item.goods_art_no]})" v-log="{ describe: { action: '删除货号', goods_art_no: item.goods_art_no } }">删除</el-button>
                   </div>
                 </div>
                 <div class="flex  between flex-item  c-333" style="margin-top: 5px">
                   <div class="c-999 fs-12">{{ getTime(item.action_time) }}</div>
-                  <el-button size="small" :disabled="runLoading || takePictureLoading"  type="primary"  @click="reTakePictureNos(item.goods_art_no,item)" plain v-if="configInfoStore.appModel === 1">重拍</el-button>
+                  <el-button size="small" :disabled="runLoading || takePictureLoading"  type="primary"  @click="reTakePictureNos(item.goods_art_no,item)" plain v-if="configInfoStore.appModel === 1" v-log="{ describe: { action: '重拍货号', goods_art_no: item.goods_art_no } }">重拍</el-button>
 
                 </div>
                 <div class="mar-top-10 clearfix history-item_image_wrap" style="width: 100%" >
@@ -147,7 +147,7 @@
                                 <div class="image-slot"></div>
                               </template>
                             </el-image>
-                            <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)">重拍</el-button>
+                            <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)" v-log="{ describe: { action: '重拍单张图片', goods_art_no: image.PhotoRecord.goods_art_no, action_name: image.action_name } }">重拍</el-button>
                           </div>
                         </template>
 
@@ -164,7 +164,7 @@
                       <el-image :src="getFilePath(image.PhotoRecord.image_path)"  fit="contain">
                         <template #error>
                           <div class="image-slot"></div>
-                          <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)">重拍</el-button>
+                          <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)" v-log="{ describe: { action: '重拍单张图片', goods_art_no: image.PhotoRecord.goods_art_no, action_name: image.action_name } }">重拍</el-button>
                         </template>
                       </el-image>
                       </div>
@@ -214,10 +214,12 @@ const loading = ref(false)
 const runLoading = ref(false)
 const takePictureLoading = ref(false)
 import { Close } from '@element-plus/icons-vue'
-
+import { clickLog, setLogInfo } from '@/utils/log'
+import { useRoute } from 'vue-router'
 
 import useUserInfo from "@/stores/modules/user";
 const useUserInfoStore = useUserInfo();
+const route = useRoute();
 
 import  configInfo  from '@/stores/modules/config';
 const configInfoStore = configInfo();
@@ -722,6 +724,7 @@ onMounted(async () => {
     console.log('_photo_take_finish')
     console.log(result)
     if(result.code === 0) {
+      setLogInfo(route, { action: '拍摄完成', goods_art_no: runAction.value.goods_art_no });
       runLoading.value = false;
       runAction.value.goods_art_no = '';
       runAction.value.action = '';
@@ -766,6 +769,8 @@ onMounted(async () => {
 
 const onRemoteControl = (type)=>{
   if(type == 'take_picture'){
+    // 埋点:手动拍照
+    clickLog({ describe: { action: '手动拍照' } }, route);
 
     if(runLoading.value || takePictureLoading.value){
       ElMessage.error('拍摄程序正在运行,请稍候')
@@ -787,6 +792,10 @@ const onRemoteControl = (type)=>{
   }
   let action = '执行左脚程序'
   if(type  === 'right')  action = '执行右脚程序'
+
+  // 埋点:遥控器启动拍摄
+  clickLog({ describe: { action: `遥控器${type === 'left' ? '左脚' : '右脚'}启动拍摄`, goods_art_no: goods_art_no.value } }, route);
+
   runGoods({
     "action": action,
     "goods_art_no": goods_art_no.value,
@@ -956,6 +965,8 @@ clientStore.ipc.on(icpList.socket.message + '_run_mcu_update', (event, result) =
  * 打开主图详情页面。
  */
 function openPhotographyDetail() {
+  // 埋点:开始生成
+  clickLog({ describe: { action: '开始生成', goods_count: goodsList.value.length, goods_art_nos: goodsList.value.map(item=>item.goods_art_no) } }, route);
 
   if(runLoading.value || takePictureLoading.value){
     ElMessage.error('正在拍摄中,请稍候')

+ 2 - 2
frontend/src/views/RemoteControl/index.vue

@@ -17,11 +17,11 @@
     <el-row align="middle">
       <el-col :span="6"></el-col>
       <el-col :span="6">
-        <div class="button up" @click.native="switchLED(1)" >LED开</div>
+        <div class="button up" @click="switchLED(1)" v-log="{ describe: { action: 'LED开启' } }">LED开</div>
       </el-col>
       <el-col :span="1"></el-col>
       <el-col :span="6">
-        <div class="button up" @click.native="switchLED(0)">LED关</div>
+        <div class="button up" @click="switchLED(0)" v-log="{ describe: { action: 'LED关闭' } }">LED关</div>
       </el-col>
       <el-col :span="4"></el-col>
     </el-row>