├── .gitignore ├── tsconfig.node.json ├── index.html ├── src ├── router │ ├── index.ts │ └── router.dynamic.ts ├── components │ ├── index.ts │ ├── MenuList.vue │ └── CTable.vue ├── views │ ├── home │ │ └── index.vue │ ├── pv │ │ └── index.vue │ ├── event │ │ └── index.vue │ ├── intersection │ │ └── index.vue │ ├── http │ │ └── index.vue │ ├── performance │ │ └── index.vue │ └── err │ │ └── index.vue ├── assets │ └── global.scss ├── main.ts ├── App.vue └── utils │ └── sourcemap.ts ├── README.md ├── tsconfig.json ├── .eslintrc.cjs ├── package.json ├── vite.config.ts ├── public └── vite.svg └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | types 4 | examples-copy -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["./vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | example-vue3 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | createWebHashHistory 4 | // createWebHistory 5 | } from 'vue-router' 6 | import { dynamicRouterMap } from './router.dynamic' 7 | 8 | const router = createRouter({ 9 | history: createWebHashHistory(), 10 | routes: dynamicRouterMap, 11 | scrollBehavior() { 12 | return { 13 | top: 0, 14 | behavior: 'smooth' 15 | } 16 | } 17 | }) 18 | 19 | export { router as default, dynamicRouterMap } 20 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | 3 | interface ObjType { 4 | [propName: string]: object 5 | } 6 | interface filesType extends ObjType { 7 | default: { 8 | __name: string 9 | [key: string]: any 10 | } 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 14 | // @ts-ignore 15 | const files: Record = import.meta.globEager('./*.vue') 16 | 17 | export default (app: App) => { 18 | Object.keys(files).forEach(path => { 19 | const name = files[path].default.name 20 | app.component(name, files[path].default) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 | 4 |

web-tracing 监控插件

5 |

6 | 基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段 7 |

8 |
9 | 10 | ## web-tracing-examples-vue3 11 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(vue3版本) 12 | 13 | 此项目由 [web-tracing -> examples -> vue3](https://github.com/M-cheng-web/web-tracing/tree/main/examples/vue3) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的 14 | 15 | 因为是直接强推的代码,所以本地更新时需要执行: 16 | ``` 17 | git fetch --all 18 | git reset --hard origin/main 19 | ``` 20 | 21 | ## 运行 22 | ``` 23 | pnpm install 24 | pnpm run start 25 | ``` 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "paths": { 16 | "@web-tracing/core": ["../../packages/core/index.ts"], 17 | "@web-tracing/core/*": ["../../packages/core/*"], 18 | // "@web-tracing/component": ["./packages/component/index.ts"], 19 | }, 20 | "types": [ 21 | "vite/client" 22 | ] 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 8 | overrides: [], 9 | parser: 'vue-eslint-parser', 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module' 13 | }, 14 | plugins: ['vue'], 15 | rules: { 16 | 'no-debugger': 'warn', 17 | 'prettier/prettier': [ 18 | 'error', 19 | { 20 | semi: false, 21 | trailingComma: 'none', 22 | arrowParens: 'avoid', 23 | singleQuote: true, 24 | endOfLine: 'auto' 25 | } 26 | ], 27 | 'vue/return-in-computed-property': 'off', 28 | 'vue/no-multiple-template-root': 'off', 29 | 'vue/multi-word-component-names': 'off', 30 | '@typescript-eslint/ban-ts-comment': 'off' 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "concurrently \"npm run dev\" \"npm run service\"", 8 | "dev": "vite", 9 | "service": "nodemon ./server.js", 10 | "build": "vite build", 11 | "build-tsc": "vue-tsc && vite build", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@web-tracing/vue3": "2.0.4", 16 | "axios": "^1.4.0", 17 | "body-parser": "^1.20.2", 18 | "co-body": "^6.1.0", 19 | "concurrently": "^8.2.0", 20 | "element-plus": "^2.3.7", 21 | "express": "^4.18.2", 22 | "nodemon": "^2.0.22", 23 | "rrweb-player": "1.0.0-alpha.4", 24 | "sass": "^1.63.6", 25 | "sass-loader": "^13.3.2", 26 | "source-map-js": "^1.0.2", 27 | "vue": "^3.2.47", 28 | "vue-router": "^4.2.2" 29 | }, 30 | "devDependencies": { 31 | "@vitejs/plugin-vue": "^4.1.0", 32 | "eslint": "^8.36.0", 33 | "eslint-config-prettier": "^8.8.0", 34 | "eslint-plugin-prettier": "^4.2.1", 35 | "eslint-plugin-vue": "^9.15.0", 36 | "prettier": "^2.8.7", 37 | "typescript": "^4.9.3", 38 | "vite": "^4.2.0", 39 | "vue-tsc": "^1.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/global.scss: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | margin: 0; 4 | -moz-osx-font-smoothing: grayscale; 5 | -webkit-font-smoothing: antialiased; 6 | text-rendering: optimizeLegibility; 7 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 8 | } 9 | 10 | label { 11 | font-weight: 700; 12 | } 13 | 14 | html { 15 | height: 100%; 16 | box-sizing: border-box; 17 | } 18 | 19 | .mb { 20 | margin-bottom: 20px; 21 | } 22 | 23 | .event-pop { 24 | .pop-line:first-child { 25 | border-bottom: 1px solid #0984e3; 26 | } 27 | .pop-line:not(:first-child) { 28 | & > div { 29 | color: #0984e3; 30 | } 31 | } 32 | .pop-line { 33 | position: relative; 34 | display: flex; 35 | align-items: center; 36 | & > span { 37 | position: absolute; 38 | left: -16px; 39 | } 40 | & > div { 41 | flex: 1; 42 | white-space: nowrap; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | } 46 | } 47 | .warning-text { 48 | color: red; 49 | } 50 | } 51 | 52 | #app { 53 | height: 100%; 54 | } 55 | 56 | *, 57 | *:before, 58 | *:after { 59 | box-sizing: inherit; 60 | } 61 | 62 | a:focus, 63 | a:active { 64 | outline: none; 65 | } 66 | 67 | a, 68 | a:focus, 69 | a:hover { 70 | cursor: pointer; 71 | color: inherit; 72 | text-decoration: none; 73 | } 74 | 75 | div:focus { 76 | outline: none; 77 | } 78 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | server: { 9 | https: false, 10 | host: '0.0.0.0', 11 | port: 6657, 12 | cors: true, 13 | proxy: { 14 | '/getList': { 15 | target: 'http://localhost:3352/', 16 | changeOrigin: false, // target是域名的话,需要这个参数, 17 | secure: false // 设置支持https协议的代理, 18 | }, 19 | '/setList': { 20 | target: 'http://localhost:3352/', 21 | changeOrigin: false, // target是域名的话,需要这个参数, 22 | secure: false // 设置支持https协议的代理, 23 | }, 24 | '/cleanTracingList': { 25 | target: 'http://localhost:3352/', 26 | changeOrigin: false, 27 | secure: false 28 | }, 29 | '/getBaseInfo': { 30 | target: 'http://localhost:3352' 31 | }, 32 | '/getAllTracingList': { 33 | target: 'http://localhost:3352' 34 | }, 35 | '/trackweb': { 36 | target: 'http://localhost:3352' 37 | }, 38 | '/getSourceMap': { 39 | target: 'http://localhost:3352/', 40 | changeOrigin: false, // target是域名的话,需要这个参数, 41 | secure: false // 设置支持https协议的代理, 42 | } 43 | } 44 | }, 45 | resolve: { 46 | alias: { 47 | '@': resolve(__dirname, 'src') 48 | } 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/router/router.dynamic.ts: -------------------------------------------------------------------------------- 1 | export const dynamicRouterMap = [ 2 | { 3 | path: '/', 4 | redirect: '/home' 5 | }, 6 | { 7 | path: '/home', 8 | name: 'Home', 9 | component: () => import('@/views/home/index.vue'), 10 | meta: { 11 | title: '首页', 12 | icon: 'el-icon-setting' 13 | } 14 | }, 15 | { 16 | path: '/err', 17 | name: 'Err', 18 | component: () => import('@/views/err/index.vue'), 19 | meta: { 20 | title: '监控 - 错误', 21 | icon: 'el-icon-setting' 22 | } 23 | }, 24 | { 25 | path: '/event', 26 | name: 'Event', 27 | component: () => import('@/views/event/index.vue'), 28 | meta: { 29 | title: '监控 - 点击事件', 30 | icon: 'el-icon-setting' 31 | } 32 | }, 33 | { 34 | path: '/http', 35 | name: 'Http', 36 | component: () => import('@/views/http/index.vue'), 37 | meta: { 38 | title: '监控 - 请求', 39 | icon: 'el-icon-setting' 40 | } 41 | }, 42 | { 43 | path: '/performance', 44 | name: 'Performance', 45 | component: () => import('@/views/performance/index.vue'), 46 | meta: { 47 | title: '监控 - 资源', 48 | icon: 'el-icon-setting' 49 | } 50 | }, 51 | { 52 | path: '/pv', 53 | name: 'Pv', 54 | component: () => import('@/views/pv/index.vue'), 55 | meta: { 56 | title: '监控 - 页面跳转', 57 | icon: 'el-icon-setting' 58 | } 59 | }, 60 | { 61 | path: '/intersection', 62 | name: 'intersection', 63 | component: () => import('@/views/intersection/index.vue'), 64 | meta: { 65 | title: '监控 - 曝光采集', 66 | icon: 'el-icon-setting' 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /src/components/MenuList.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | 46 | 60 | 61 | 67 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import ElementPlus from 'element-plus' 4 | import 'element-plus/dist/index.css' 5 | import WebTracing from '@web-tracing/vue3' 6 | import router from './router' 7 | import './assets/global.scss' 8 | import initComponents from './components/index' 9 | import { ElNotification } from 'element-plus' 10 | 11 | const app = createApp(App) 12 | 13 | const sendEventType: any = { 14 | pv: '路由', 15 | error: '错误', 16 | performance: '资源', 17 | click: '点击', 18 | dwell: '页面卸载', 19 | intersection: '曝光采集' 20 | } 21 | 22 | app.use(WebTracing, { 23 | dsn: '/trackweb', 24 | appName: 'cxh', 25 | debug: true, 26 | pv: true, 27 | performance: true, 28 | error: true, 29 | event: true, 30 | cacheMaxLength: 10, 31 | cacheWatingTime: 1000, 32 | 33 | // 查询埋点信息、清除埋点信息、获取埋点基础信息 不需要进行捕获 34 | ignoreRequest: [ 35 | /getAllTracingList/, 36 | /cleanTracingList/, 37 | /getBaseInfo/, 38 | /getSourceMap/ 39 | ], 40 | 41 | // 发送埋点数据后,拉起弹窗提示用户已发送 42 | afterSendData(data) { 43 | const { sendType, success, params } = data 44 | const message = ` 45 |
46 |
打开控制台可查看更多详细信息
47 |
发送是否成功: ${success}
48 |
发送方式: ${sendType}
49 |
发送内容(只概括 eventType、eventId) 50 | ${params.eventInfo.reduce( 51 | (pre: string, item: any, index: number) => { 52 | pre += ` 53 |
54 | ${index + 1} 55 |
${item.eventType}(${sendEventType[item.eventType]})
56 |
${item.eventId}
57 |
` 58 | return pre 59 | }, 60 | `
61 |
eventType
62 |
eventId
63 |
` 64 | )} 65 |
66 |
67 | ` 68 | ElNotification({ 69 | title: '发送一批数据到服务端', 70 | message, 71 | position: 'top-right', 72 | dangerouslyUseHTMLString: true 73 | }) 74 | // @ts-ignore 75 | if (window.getAllTracingList) { 76 | // @ts-ignore 77 | window.getAllTracingList() 78 | } 79 | } 80 | }) 81 | 82 | app.use(router) 83 | app.use(initComponents) 84 | app.use(ElementPlus) 85 | app.mount('#app') 86 | -------------------------------------------------------------------------------- /src/views/pv/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 93 | 94 | 98 | -------------------------------------------------------------------------------- /src/components/CTable.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 77 | 78 | 115 | 116 | 136 | -------------------------------------------------------------------------------- /src/views/event/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 109 | 110 | 135 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 109 | 110 | 138 | 139 | 151 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import path from 'path' 3 | import fs from 'fs' 4 | import pkg from 'body-parser' 5 | import coBody from 'co-body' 6 | 7 | const app = express() 8 | const { json, urlencoded } = pkg 9 | 10 | app.use(json({ limit: '100mb' })) 11 | app.use( 12 | urlencoded({ 13 | limit: '100mb', 14 | extended: true, 15 | parameterLimit: 50000 16 | }) 17 | ) 18 | 19 | app.all('*', function (res, req, next) { 20 | req.header('Access-Control-Allow-Origin', '*') 21 | req.header('Access-Control-Allow-Headers', 'Content-Type') 22 | req.header('Access-Control-Allow-Methods', '*') 23 | req.header('Content-Type', 'application/json;charset=utf-8') 24 | next() 25 | }) 26 | 27 | // 获取js.map源码文件 28 | app.get('/getSourceMap', (req, res) => { 29 | const { fileName, env } = req.query 30 | console.log('fileName', fileName) 31 | console.log('env', env) 32 | if (env === 'development') { 33 | // const mapFile = path.join(__filename, '..', fileName) 34 | // console.log('mapFile', mapFile) 35 | fs.readFile(fileName, (err, data) => { 36 | if (err) { 37 | console.error('server-getmap', err) 38 | return 39 | } 40 | res.send(data) 41 | }) 42 | } else { 43 | // req.query 获取接口参数 44 | const mapFile = path.join(__filename, '..', 'dist/assets') 45 | // 拿到dist目录下对应map文件的路径 46 | const mapPath = path.join(mapFile, `${fileName}.map`) 47 | fs.readFile(mapPath, (err, data) => { 48 | if (err) { 49 | console.error('server-getmap', err) 50 | return 51 | } 52 | res.send(data) 53 | }) 54 | } 55 | }) 56 | 57 | app.get('/getList', (req, res) => { 58 | console.log('req.query', req.query) 59 | res.send({ 60 | code: 200, 61 | data: [1, 2, 3] 62 | }) 63 | }) 64 | app.post('/setList', (req, res) => { 65 | res.send({ 66 | code: 200, 67 | meaage: '设置成功' 68 | }) 69 | }) 70 | 71 | let allTracingList = [] 72 | let baseInfo = {} 73 | 74 | app.get('/getBaseInfo', (req, res) => { 75 | res.send({ 76 | code: 200, 77 | data: baseInfo 78 | }) 79 | }) 80 | app.post('/cleanTracingList', (req, res) => { 81 | allTracingList = [] 82 | res.send({ 83 | code: 200, 84 | meaage: '清除成功!' 85 | }) 86 | }) 87 | app.get('/getAllTracingList', (req, res) => { 88 | const eventType = req.query.eventType 89 | if (eventType) { 90 | // const data = JSON.parse(JSON.stringify(allTracingList)).reverse() 91 | const data = JSON.parse(JSON.stringify(allTracingList)) 92 | res.send({ 93 | code: 200, 94 | data: data.filter(item => item.eventType === eventType) 95 | }) 96 | } else { 97 | res.send({ 98 | code: 200, 99 | data: allTracingList 100 | }) 101 | } 102 | }) 103 | app.post('/trackweb', async (req, res) => { 104 | try { 105 | let length = Object.keys(req.body).length 106 | if (length) { 107 | // 数据量大时不会用 sendbeacon,会用xhr的形式,这里是接收xhr的数据格式 108 | allTracingList.push(...req.body.eventInfo) 109 | baseInfo = req.body.baseInfo 110 | } else { 111 | // 兼容 sendbeacon 的传输数据格式 112 | const data = await coBody.json(req) 113 | if (!data) return 114 | allTracingList.push(...data.eventInfo) 115 | baseInfo = data.baseInfo 116 | } 117 | res.send({ 118 | code: 200, 119 | meaage: '上报成功!' 120 | }) 121 | } catch (err) { 122 | res.send({ 123 | code: 203, 124 | meaage: '上报失败!', 125 | err 126 | }) 127 | } 128 | }) 129 | 130 | // 图片上传的方式 131 | app.get('/trackweb', async (req, res) => { 132 | try { 133 | let data = req.query.v 134 | if (!data) return 135 | data = JSON.parse(data) 136 | allTracingList.push(...data.eventInfo) 137 | baseInfo = data.baseInfo 138 | res.send({ 139 | code: 200, 140 | data: '上报成功' 141 | }) 142 | } catch (err) { 143 | res.send({ 144 | code: 203, 145 | meaage: '上报失败!', 146 | err 147 | }) 148 | } 149 | }) 150 | 151 | app.listen(3352, () => { 152 | console.log('Server is running at http://localhost:3352') 153 | }) 154 | -------------------------------------------------------------------------------- /src/utils/sourcemap.ts: -------------------------------------------------------------------------------- 1 | import sourceMap from 'source-map-js' 2 | 3 | // 找到以.js结尾的fileName 4 | function matchStr(str: string) { 5 | if (str.endsWith('.js')) return str.substring(str.lastIndexOf('/') + 1) 6 | } 7 | 8 | // 将所有的空格转化为实体字符 9 | function repalceAll(str: string) { 10 | return str.replace(new RegExp(' ', 'gm'), ' ') 11 | } 12 | 13 | // 获取文件路径 14 | function getFileLink(str: string) { 15 | const reg = /vue-loader-options!\.(.*)\?/ 16 | const res = str.match(reg) 17 | console.log(res, 'getFileLink') 18 | if (res && Array.isArray(res)) { 19 | return res[1] 20 | } 21 | } 22 | 23 | function loadSourceMap(fileName: string): Promise | string { 24 | const env: string = import.meta.env.MODE 25 | const file = fileName 26 | 27 | // if (env == 'development') { 28 | // file = getFileLink(fileName) 29 | // } else { 30 | // file = matchStr(fileName) 31 | // } 32 | 33 | console.log('file', file) 34 | if (!file) return '' 35 | return new Promise(resolve => { 36 | fetch( 37 | `http://localhost:3352/getSourceMap?fileName=${file}&env=${env}` 38 | ).then(response => { 39 | console.log('response', response) 40 | if (env == 'development') { 41 | resolve(response.text()) 42 | } else { 43 | resolve(response.json()) 44 | } 45 | }) 46 | }) 47 | } 48 | 49 | interface mapOptions { 50 | fileName: string 51 | line: number 52 | column: number 53 | } 54 | export const findCodeBySourceMap = async ( 55 | { fileName, line, column }: mapOptions, 56 | callback: any 57 | ) => { 58 | const sourceData = await loadSourceMap(fileName) 59 | if (!sourceData) return 60 | let result, codeList 61 | if (import.meta.env.MODE == 'development') { 62 | const source = getFileLink(fileName) 63 | let isStart = false 64 | result = { 65 | source, 66 | line: line + 1, // 具体的报错行数 67 | column, // 具体的报错列数 68 | name: null 69 | } 70 | codeList = (sourceData as string).split('\n').filter(item => { 71 | if (item.indexOf(' 106 | item.replace(/\/.\//g, '/') 107 | ) 108 | index = copySources.indexOf(result.source) 109 | } 110 | console.log('index', index) 111 | if (index === -1) { 112 | return '源码解析失败' 113 | } 114 | const code = sourcesContent[index] 115 | codeList = code.split('\n') 116 | } 117 | 118 | const row = result.line 119 | const len = codeList.length - 1 120 | const start = row - 5 >= 0 ? row - 5 : 0 // 将报错代码显示在中间位置 121 | const end = start + 9 >= len ? len : start + 9 // 最多展示10行 122 | const newLines = [] 123 | let j = 0 124 | for (let i = start; i <= end; i++) { 125 | j++ 126 | newLines.push( 127 | `
${j}. ${repalceAll(codeList[i])}
` 130 | ) 131 | } 132 | 133 | const innerHTML = `
${ 134 | result.source 135 | } at line ${result.column}:${row}
${newLines.join( 136 | '' 137 | )}
` 138 | 139 | callback(innerHTML) 140 | } 141 | -------------------------------------------------------------------------------- /src/views/intersection/index.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 173 | 174 | 178 | -------------------------------------------------------------------------------- /src/views/http/index.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /src/views/performance/index.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 202 | 203 | 209 | -------------------------------------------------------------------------------- /src/views/err/index.vue: -------------------------------------------------------------------------------- 1 | 225 | 226 | 430 | 431 | 469 | --------------------------------------------------------------------------------