zhengyiming
3 天以前 5664a1a616df498cba58b9a8e63a91ac0ba96bab
feat: add rrweb
1个文件已添加
4个文件已修改
152 ■■■■■ 已修改文件
.env.development 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/record.ts 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Home/Home.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -2,7 +2,7 @@
VITE_PORT = 8698
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH = /front/
VITE_PUBLIC_PATH = /
# 开发环境代理
VITE_PROXY_DOMAIN = /api
package.json
@@ -72,6 +72,7 @@
    "pinia": "^2.2.4",
    "qs": "^6.11.0",
    "rgb-hex": "^4.0.0",
    "rrweb": "2.0.0-alpha.4",
    "semver": "^7.6.3",
    "senin-help": "latest",
    "senin-vue": "latest",
pnpm-lock.yaml
@@ -139,6 +139,9 @@
  rgb-hex:
    specifier: ^4.0.0
    version: 4.0.0
  rrweb:
    specifier: 2.0.0-alpha.4
    version: 2.0.0-alpha.4
  semver:
    specifier: ^7.6.3
    version: 7.6.3
@@ -3627,6 +3630,10 @@
    dev: true
    optional: true
  /@rrweb/types@2.0.0-alpha.18:
    resolution: {integrity: sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==}
    dev: false
  /@sindresorhus/merge-streams@2.3.0:
    resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
    engines: {node: '>=18'}
@@ -3949,6 +3956,10 @@
  /@types/ali-oss@6.16.11:
    resolution: {integrity: sha512-/AyemPZy93ZXGzEokMsoPFgjH37snpzH4X/fwans/n63HLaCleriCG3PyrkHCPkgHEc9vj9Uo6paqsBN3vJ3OA==}
    dev: true
  /@types/css-font-loading-module@0.0.7:
    resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
    dev: false
  /@types/eslint@7.29.0:
    resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
@@ -5182,6 +5193,10 @@
    engines: {node: '>=10.0.0'}
    dev: false
  /@xstate/fsm@1.6.5:
    resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
    dev: false
  /@ywwlmm/openapi@0.0.3:
    resolution: {integrity: sha512-zbQMq2LnMX2IJmEJIG12IeggQCEm1sn7E+S7TH6yCLaYOERf8ARA22GJUmjmYaLUD9iKgb8yl2ohO3PSI6aMdw==}
    dependencies:
@@ -5782,6 +5797,11 @@
  /balanced-match@2.0.0:
    resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
  /base64-arraybuffer@1.0.2:
    resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
    engines: {node: '>= 0.6.0'}
    dev: false
  /base64-js@1.5.1:
    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -8502,6 +8522,10 @@
    resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
    dependencies:
      reusify: 1.0.4
  /fflate@0.4.8:
    resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
    dev: false
  /file-entry-cache@4.0.0:
    resolution: {integrity: sha512-AVSwsnbV8vH/UVbvgEhf3saVQXORNv0ZzSkvkhQIaia5Tia+JhGTaa/ePUSVoPHQyGayQNmYfkzFi3WZV5zcpA==}
@@ -13408,6 +13432,29 @@
    resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
    dev: false
  /rrdom@0.1.7:
    resolution: {integrity: sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==}
    dependencies:
      rrweb-snapshot: 2.0.0-alpha.4
    dev: false
  /rrweb-snapshot@2.0.0-alpha.4:
    resolution: {integrity: sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==}
    dev: false
  /rrweb@2.0.0-alpha.4:
    resolution: {integrity: sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==}
    dependencies:
      '@rrweb/types': 2.0.0-alpha.18
      '@types/css-font-loading-module': 0.0.7
      '@xstate/fsm': 1.6.5
      base64-arraybuffer: 1.0.2
      fflate: 0.4.8
      mitt: 3.0.0
      rrdom: 0.1.7
      rrweb-snapshot: 2.0.0-alpha.4
    dev: false
  /rtc-ai-denoiser@1.1.7:
    resolution: {integrity: sha512-53e/4a4lT96K004mqDnLDE+upNSpBLRMfFgYCeIw3Gvuw9F17nxLP5v8MOVLly4/Epomxkx4SXrOFJJMxD2pIw==}
    dev: false
src/utils/record.ts
New file
@@ -0,0 +1,91 @@
import * as rrweb from 'rrweb';
import { guid } from './common';
import { eventWithTime, listenerHandler } from '@rrweb/types';
import { storageLocal } from './storage';
export class Recorder {
  // 生成会话ID(用于标识一次投保流程)
  sessionId = guid();
  // 存储录制事件的数组
  events = [];
  stopFn: () => void;
  // 初始化录制器
  init() {
    this.stopFn = rrweb.record({
      emit: (event) => {
        // 收集事件(可选择实时发送到后端)
        this.events.push(event);
        // 定期发送事件到后端(避免内存占用过大)
        if (this.events.length % 100 === 0) {
          this.sendEventsToBackend(this.events.splice(0, 100), this.sessionId);
        }
      },
      // 可选配置:忽略敏感元素
      blockClass: 'rr-block', // 为敏感元素添加此class
      ignoreClass: 'rr-ignore', // 完全忽略的元素
    });
  }
  // 结束录制(例如提交投保表单时)
  stopRecordingAndSave() {
    this.stopFn?.();
    // 发送剩余事件到后端
    if (this.events.length > 0) {
      this.sendEventsToBackend(this.events, this.sessionId);
    }
    // 存储会话ID(关联到用户投保记录)
    this.saveSessionIdToUserRecord(this.sessionId);
  }
  saveSessionIdToUserRecord(sessionId: string) {}
  async sendEventsToBackend(events: eventWithTime[], sessionId: string) {
    try {
      const body = JSON.stringify({
        sessionId,
        events,
        timestamp: new Date().toISOString(),
      });
      console.log('body: ', body);
      const storageEvents = storageLocal.getItem(`events`) || {};
      storageEvents[sessionId] = events;
      storageLocal.setItem(`events`, storageEvents);
      // await fetch('/api/recordings', {
      //   method: 'POST',
      //   headers: {
      //     'Content-Type': 'application/json',
      //   },
      //   body: ,
      // });
    } catch (error) {
      console.error('Failed to send recording events:', error);
      // 可实现本地存储作为降级方案
    }
  }
  async replaySession(sessionId: string) {
    // 从后端获取录制事件
    // const response = await fetch(`/api/recordings/${sessionId}`);
    // const { events } = await response.json();
    const storageEvents = storageLocal.getItem(`events`) || {};
    if (!storageEvents[sessionId]) {
      return;
    }
    // 初始化回放器
    const player = new rrweb.Replayer(storageEvents[sessionId], {
      root: document.getElementById('replay-container'),
    });
    // 可选:控制回放速度
    player.play(1.0); // 1倍速
    // 可选:监听回放事件
    player.on('play', () => console.log('Replay started'));
    player.on('pause', () => console.log('Replay paused'));
    player.on('finish', () => console.log('Replay finished'));
  }
}
src/views/Home/Home.vue
@@ -155,6 +155,7 @@
import dayjs from 'dayjs';
import _ from 'lodash';
import InsureInstructionsDialog from './components/InsureInstructionsDialog.vue';
// import { Recorder } from '@/utils/record';
defineOptions({
  name: 'Home',
@@ -262,13 +263,23 @@
};
const state = reactive({ ...BaseState });
// const recorder = ref(new Recorder());
onMounted(async () => {
  await getList();
  state.loading = false;
  handleOpenInstructions();
  // setTimeout(() => {
  //   // recorder.value.init();
  //   recorder.value.replaySession('9cb24e5a-0423-4dcd-abd5-fa7a4117cadc');
  // }, 3000);
});
// onUnmounted(() => {
//   recorder.value.stopRecordingAndSave();
// });
const {
  getDataSource: getList,
  proTableProps,