├── .npmrc ├── .yarnrc ├── .eslintignore ├── .gitattributes ├── .browserslistrc ├── docs ├── .babelrc ├── dist │ ├── favicon.ico │ ├── fonts │ │ ├── element-icons.535877f5.woff │ │ └── element-icons.732389de.ttf │ ├── css │ │ ├── app.2e14d2d6.css │ │ └── chunk-vendors.c687a9b2.css │ └── js │ │ └── app.efe84ade.js ├── assets │ ├── index.scss │ ├── mixins.scss │ ├── app.scss │ └── normalize.scss ├── tracks │ ├── index.js │ ├── action.js │ └── events.js ├── components │ ├── button.vue │ └── code-snippet.vue ├── utils │ ├── dom.js │ └── date.js ├── index.html ├── main.js ├── App.vue └── pages │ ├── started.vue │ ├── block-show.vue │ ├── track-view.vue │ ├── home.vue │ └── custom-events.vue ├── public ├── favicon.ico └── index.html ├── tests ├── unit │ ├── .eslintrc.js │ ├── track-view.spec.js │ ├── track-click.spec.js │ ├── track-click-delay.spec.js │ ├── track-custom-event.spec.js │ ├── track-show-param.spec.js │ ├── track-view-vif.spec.js │ ├── track-click-async.spec.js │ ├── track-click-param.spec.js │ ├── track-custom-event-delay.spec.js │ ├── track-view-watch.spec.js │ ├── track-show-once.spec.js │ ├── track-show.spec.js │ ├── track-custom-event-async.spec.js │ ├── track-view-watch-delay.spec.js │ ├── track-custom-event-param.spec.js │ ├── track-show-custom-scroll-once.spec.js │ ├── track-show-custom-scroll.spec.js │ └── track-click-native.spec.js └── helper.js ├── postcss.config.js ├── .travis.yml ├── .gitignore ├── src ├── utils │ ├── debug.js │ ├── dom.js │ ├── helper.js │ └── vis-monitor.js ├── index.js └── hooks │ └── index.js ├── .eslintrc.js ├── babel.config.js ├── jest.config.js ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── LICENSE ├── vue.config.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.yarnpkg.com" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | output.js -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/* linguist-documentation=false -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@vue/app" 4 | ] 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l-hammer/v-track/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /docs/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l-hammer/v-track/HEAD/docs/dist/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /docs/dist/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l-hammer/v-track/HEAD/docs/dist/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /docs/dist/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l-hammer/v-track/HEAD/docs/dist/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /docs/assets/index.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #40b883; 2 | $md-grey: #9e9e9e; 3 | $md-grey-100: #f5f5f5; 4 | $md-grey-200: #eeeeee; 5 | $md-grey-300: #e0e0e0; 6 | $md-grey-400: #bdbdbd; 7 | 8 | @import './normalize.scss'; 9 | @import './mixins.scss'; 10 | @import './app.scss'; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | yarn: true 5 | 6 | node_js: 7 | - '10' 8 | - '8' 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | install: 15 | - yarn 16 | 17 | script: 18 | - yarn lint 19 | - yarn test:unit 20 | 21 | after_success: 22 | - yarn build 23 | -------------------------------------------------------------------------------- /docs/tracks/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-14 17:01:49 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-14 17:12:29 6 | */ 7 | import trackEvents from "./events"; 8 | 9 | export { default as trackAction } from "./action"; 10 | 11 | export default { 12 | ...trackEvents 13 | }; 14 | -------------------------------------------------------------------------------- /docs/components/button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | output.js 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | -------------------------------------------------------------------------------- /src/utils/debug.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-14 15:55:15 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-20 18:06:31 6 | */ 7 | import { isFun } from "./helper"; 8 | 9 | export const checkFun = fn => { 10 | if (!isFun(fn)) { 11 | throw new Error("The first parameter should be Function."); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 10 | "exclude": /(dist)/ 11 | }, 12 | parserOptions: { 13 | parser: "babel-eslint" 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | modules: false 7 | } 8 | ] 9 | ], 10 | plugins: [ 11 | "@babel/plugin-proposal-class-properties", 12 | [ 13 | "component", 14 | { 15 | libraryName: "element-ui", 16 | styleLibraryName: "theme-chalk" 17 | } 18 | ] 19 | ], 20 | env: { 21 | test: { 22 | presets: ["@babel/preset-env"] 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.jsx?$": "babel-jest" 8 | }, 9 | transformIgnorePatterns: ["/node_modules/"], 10 | moduleNameMapper: { 11 | "^@/(.*)$": "/src/$1" 12 | }, 13 | snapshotSerializers: ["jest-serializer-vue"], 14 | testMatch: [ 15 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 16 | ], 17 | testURL: "http://localhost/" 18 | }; 19 | -------------------------------------------------------------------------------- /docs/utils/dom.js: -------------------------------------------------------------------------------- 1 | export const createFragment = data => { 2 | const tableData = Object.keys(data).reduce( 3 | (list, k) => (list.push({ key: k, val: data[k] }), list), 4 | [] 5 | ); 6 | const trs = tableData 7 | .map(({ key, val }) => { 8 | return ` 9 |
10 | ${key} 11 | ${val} 12 |
13 | `; 14 | }) 15 | .join(""); 16 | 17 | return ` 18 |
19 |
20 | key 21 | value 22 |
23 | ${trs} 24 |
25 | `; 26 | }; 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | v-track 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://raw.githubusercontent.com/l-hammer/You-need-to-know-css/master/static/reward-code.png'] 13 | -------------------------------------------------------------------------------- /tests/unit/track-view.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-10 21:28:53 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-12 22:51:40 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(() => true); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const TrackView = Vue.extend({ 17 | template: ` 18 | 19 | ` 20 | }); 21 | 22 | localVue.use(VTrack, { 23 | trackEvents 24 | }); 25 | 26 | describe("TrackView", () => { 27 | it("确保初始化埋点正常上报", () => { 28 | mount(TrackView, { localVue }); 29 | expect(mockTrackAction).toBeTruthy(); 30 | expect(mockTrackAction).toBeCalledTimes(1); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: yarn 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-13 14:11:11 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 15:05:55 6 | */ 7 | export const mockParentNode = el => { 8 | // mock dom parentNode 9 | Object.defineProperty(el, "parentNode", { 10 | get: () => document 11 | }); 12 | }; 13 | 14 | export const mockRect = el => { 15 | // https://github.com/jsdom/jsdom/issues/653 16 | el.getBoundingClientRect = () => ({ 17 | width: 100, 18 | height: 100, 19 | top: 50, 20 | left: 50, 21 | right: 50, 22 | bottom: 50 23 | }); 24 | }; 25 | 26 | export const mockScrollTo = () => { 27 | // eslint-disable-next-line no-undef 28 | Element.prototype.scrollTo = jest 29 | .fn() 30 | .mockImplementation((x = 50, y = 50) => { 31 | const w = window; 32 | w.scrollX = x; 33 | w.scrollY = y; 34 | w.pageXOffset = x; 35 | w.pageYOffset = y; 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | v-track
-------------------------------------------------------------------------------- /docs/tracks/action.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-14 16:44:42 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-07-30 20:17:13 6 | */ 7 | import { Notification, Message } from "element-ui"; 8 | import { format } from "../utils/date"; 9 | import { createFragment } from "../utils/dom"; 10 | 11 | /** 12 | * @desc 模拟埋点Action 13 | */ 14 | export default function trackAction(evt, addtional = {}) { 15 | const data = { 16 | evt, 17 | ...addtional, 18 | action_time: format(Date.now()) 19 | }; 20 | 21 | if (evt === "1") { 22 | Message("统计UVPV埋点"); 23 | } 24 | if (evt === "2") { 25 | Message({ 26 | message: "统计页面停留时间埋点", 27 | customClass: "message-offset" 28 | }); 29 | } 30 | Notification.success({ 31 | title: "上报数据如下:", 32 | dangerouslyUseHTMLString: true, 33 | message: createFragment(data), 34 | customClass: "notification__large", 35 | position: evt === "1" || evt === "2" ? "top-left" : "top-right" 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LHammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const isProd = process.env.NODE_ENV !== "production"; 3 | 4 | module.exports = { 5 | lintOnSave: isProd, 6 | outputDir: "./docs/dist", 7 | publicPath: isProd ? "./" : "./dist/", 8 | indexPath: "../index.html", 9 | 10 | configureWebpack: { 11 | entry: { 12 | app: path.resolve(__dirname, "./docs/main.js") 13 | }, 14 | resolve: { 15 | alias: { 16 | "@": path.resolve(__dirname, "./docs") 17 | } 18 | }, 19 | performance: { 20 | maxEntrypointSize: 1000000, 21 | maxAssetSize: 1000000 22 | } 23 | }, 24 | 25 | chainWebpack: config => { 26 | config.module.rule("js").include.add(path.resolve(__dirname, "./docs")); 27 | 28 | config.module 29 | .rule("vue") 30 | .use("vue-loader") 31 | .loader("vue-loader") 32 | .tap(options => { 33 | options.compilerOptions.preserveWhitespace = true; 34 | return options; 35 | }); 36 | }, 37 | 38 | css: { 39 | loaderOptions: { 40 | sass: { 41 | data: `@import "~@/assets/index.scss";` 42 | } 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /tests/unit/track-click.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-09 14:54:33 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-09 18:24:43 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const TrackClick = Vue.extend({ 17 | template: ` 18 |
19 | `, 20 | data() { 21 | return { 22 | count: 0 23 | }; 24 | }, 25 | methods: { 26 | handleClick() { 27 | this.count += 1; 28 | } 29 | } 30 | }); 31 | 32 | localVue.use(VTrack, { 33 | trackEvents 34 | }); 35 | 36 | describe("TrackClick", () => { 37 | it("确保click事件、埋点事件正常触发,且只触发一次", () => { 38 | const wrapper = mount(TrackClick, { 39 | localVue 40 | }); 41 | 42 | expect(wrapper.vm.count).toBe(0); 43 | wrapper.trigger("click"); 44 | expect(wrapper.vm.count).toBe(1); 45 | expect(mockTrackAction).toBeCalledTimes(1); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/track-click-delay.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-09 21:41:47 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 22:31:03 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(({ collaps }) => collaps); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const TrackClick = Vue.extend({ 17 | template: ` 18 |
19 | `, 20 | data() { 21 | return { 22 | collaps: false 23 | }; 24 | }, 25 | methods: { 26 | handleClick() { 27 | this.collaps = !this.collaps; 28 | } 29 | } 30 | }); 31 | 32 | localVue.use(VTrack, { 33 | trackEvents 34 | }); 35 | 36 | describe("TrackClick", () => { 37 | it("确保埋点在click事件之后上报", () => { 38 | const wrapper = mount(TrackClick, { 39 | localVue 40 | }); 41 | 42 | wrapper.trigger("click"); 43 | expect(mockTrackAction).toBeTruthy(); 44 | expect(mockTrackAction).toBeCalledTimes(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /docs/assets/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin ellipsis { 2 | overflow: hidden; 3 | -ms-text-overflow: ellipsis; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | 8 | @mixin bounds($distance) { 9 | top: $distance; 10 | bottom: $distance; 11 | right: $distance; 12 | left: $distance; 13 | } 14 | 15 | @mixin overlay { 16 | position: absolute; 17 | @include bounds(0); 18 | } 19 | 20 | @mixin flex-box { 21 | display: flex; 22 | 23 | & > * { 24 | flex: auto 0 0; 25 | } 26 | } 27 | 28 | @mixin h-box { 29 | @include flex-box; 30 | flex-direction: row; 31 | } 32 | 33 | @mixin v-box { 34 | @include flex-box; 35 | flex-direction: column; 36 | } 37 | 38 | @mixin box-center { 39 | align-items: center; 40 | justify-content: center; 41 | } 42 | 43 | @mixin space-between-x($margin) { 44 | margin-right: $margin; 45 | 46 | &:last-child { 47 | margin-right: 0; 48 | } 49 | } 50 | 51 | @mixin space-between-y($margin) { 52 | margin-bottom: $margin; 53 | 54 | &:last-child { 55 | margin-bottom: 0; 56 | } 57 | } 58 | 59 | @mixin unselectable { 60 | -moz-user-select: none; 61 | -webkit-user-select: none; 62 | -ms-user-select: none; 63 | user-select: none; 64 | } -------------------------------------------------------------------------------- /tests/unit/track-custom-event.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-13 21:03:55 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 22:17:26 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const Child = Vue.extend({ 17 | template: ` 18 |
19 | ` 20 | }); 21 | const TrackCustomEvent = Vue.extend({ 22 | template: ` 23 | 24 | `, 25 | components: { 26 | child: Child 27 | }, 28 | data() { 29 | return { 30 | count: 0 31 | }; 32 | }, 33 | methods: { 34 | handleClick() { 35 | this.count += 1; 36 | } 37 | } 38 | }); 39 | 40 | localVue.use(VTrack, { 41 | trackEvents 42 | }); 43 | 44 | describe("TrackCustomEvent", () => { 45 | it("确保自定义事件、埋点事件正常触发,且只触发一次", () => { 46 | const wrapper = mount(TrackCustomEvent, { 47 | localVue 48 | }); 49 | 50 | expect(wrapper.vm.count).toBe(0); 51 | wrapper.find(Child).trigger("click"); 52 | expect(wrapper.vm.count).toBe(1); 53 | expect(mockTrackAction).toBeCalledTimes(1); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/unit/track-show-param.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-08-05 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-05 17:01:32 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import Vue from "vue"; 13 | import VTrack from "@/"; 14 | import { mount, createLocalVue } from "@vue/test-utils"; 15 | import { mockParentNode, mockRect } from "../helper"; 16 | 17 | const localVue = createLocalVue(); 18 | const mockTrackAction = jest.fn((_, id) => id); 19 | const trackEvents = { 20 | 18015: mockTrackAction 21 | }; 22 | const TrackShow = Vue.extend({ 23 | template: ` 24 |
25 | `, 26 | data() { 27 | return { 28 | id: "2019" 29 | }; 30 | } 31 | }); 32 | 33 | localVue.use(VTrack, { 34 | trackEvents 35 | }); 36 | 37 | jest.useFakeTimers(); 38 | 39 | describe("TrackShow", () => { 40 | it("确保DOM元素完全可见之后触发埋点,且至少间隔200ms执行一下", () => { 41 | const wrapper = mount(TrackShow, { localVue }); 42 | const vm = wrapper.vm; 43 | 44 | mockParentNode(vm.$el); 45 | mockRect(vm.$el); 46 | jest.runAllTimers(); 47 | 48 | expect(mockTrackAction.mock.results[0].value).toBe("2019"); 49 | expect(mockTrackAction).toBeCalledTimes(1); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/track-view-vif.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-10 21:28:53 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-10 23:18:37 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const TrackClickVIf = Vue.extend({ 17 | template: ` 18 | 19 | `, 20 | data() { 21 | return { 22 | rest: null 23 | }; 24 | }, 25 | created() { 26 | this.fetchRest(); 27 | }, 28 | methods: { 29 | async fetchRest() { 30 | const response = await Promise.resolve({ data: true }); 31 | this.rest = response.data; 32 | } 33 | } 34 | }); 35 | 36 | localVue.use(VTrack, { 37 | trackEvents 38 | }); 39 | 40 | describe("TrackClickVIf", () => { 41 | it("确保埋点在v-if条件为真之后再上报", done => { 42 | const wrapper = mount(TrackClickVIf, { 43 | localVue 44 | }); 45 | 46 | expect(wrapper.vm.rest).toBeNull(); 47 | expect(mockTrackAction).toBeCalledTimes(0); 48 | wrapper.vm.$nextTick(() => { 49 | expect(wrapper.vm.rest).toBeTruthy(); 50 | expect(mockTrackAction).toBeCalledTimes(1); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/unit/track-click-async.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-09 19:08:17 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 22:30:28 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const TrackClickAsync = Vue.extend({ 17 | template: ` 18 |
19 | `, 20 | data() { 21 | return { 22 | rest: null 23 | }; 24 | }, 25 | methods: { 26 | async fetchRest() { 27 | const response = await Promise.resolve({ data: "success" }); 28 | this.rest = response.data; 29 | } 30 | } 31 | }); 32 | 33 | localVue.use(VTrack, { 34 | trackEvents 35 | }); 36 | 37 | describe("TrackClickAsync", () => { 38 | it("确保异步的事件返回成功之后再上报埋点", done => { 39 | const wrapper = mount(TrackClickAsync, { 40 | localVue 41 | }); 42 | 43 | wrapper.trigger("click"); 44 | expect(wrapper.vm.rest).toBeNull(); 45 | expect(mockTrackAction).toBeCalledTimes(0); 46 | wrapper.vm.$nextTick(() => { 47 | expect(wrapper.vm.rest).toBe("success"); 48 | expect(mockTrackAction).toBeCalledTimes(1); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/track-click-param.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-09 18:23:04 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 21:56:46 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const TrackClickHasParam = Vue.extend({ 17 | template: ` 18 |
19 | `, 20 | data() { 21 | return { 22 | item: { 23 | id: 1 24 | }, 25 | id: 0 26 | }; 27 | }, 28 | methods: { 29 | handleClick({ id }) { 30 | this.id = id; 31 | } 32 | } 33 | }); 34 | 35 | localVue.use(VTrack, { 36 | trackEvents 37 | }); 38 | 39 | describe("TrackClickHasParam", () => { 40 | it("确保事件传参正确,并且点击事件次数等于埋点上报次数", () => { 41 | const wrapper = mount(TrackClickHasParam, { 42 | localVue 43 | }); 44 | const vm = wrapper.vm; 45 | 46 | expect(vm.id).toBe(0); 47 | wrapper.setData({ item: { id: 1 } }); 48 | wrapper.trigger("click"); 49 | expect(vm.id).toBe(1); 50 | wrapper.setData({ item: { id: 2 } }); 51 | wrapper.trigger("click"); 52 | expect(vm.id).toBe(2); 53 | expect(mockTrackAction).toBeCalledTimes(2); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/unit/track-custom-event-delay.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-13 22:00:04 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 22:30:58 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(({ status }) => status); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const Child = Vue.extend({ 17 | template: ` 18 |
19 | `, 20 | data() { 21 | return { 22 | status: true 23 | }; 24 | } 25 | }); 26 | const TrackCustomEventDelay = Vue.extend({ 27 | template: ` 28 | 29 | `, 30 | components: { 31 | child: Child 32 | }, 33 | data() { 34 | return { 35 | status: false 36 | }; 37 | }, 38 | methods: { 39 | handleExpand({ status }) { 40 | this.status = status; 41 | } 42 | } 43 | }); 44 | 45 | localVue.use(VTrack, { 46 | trackEvents 47 | }); 48 | 49 | describe("TrackCustomEventDelay", () => { 50 | it("确保埋点在自定义事件之后上报", () => { 51 | const wrapper = mount(TrackCustomEventDelay, { 52 | localVue 53 | }); 54 | 55 | wrapper.find(Child).trigger("click"); 56 | expect(mockTrackAction).toBeTruthy(); 57 | expect(mockTrackAction).toBeCalledTimes(1); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/unit/track-view-watch.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-10 21:28:53 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-10 23:13:33 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const $route = { 17 | name: "HOME", 18 | path: "/home" 19 | }; 20 | const TrackClickWatch = Vue.extend({ 21 | template: ` 22 | 23 | `, 24 | data() { 25 | return { 26 | rest: null 27 | }; 28 | }, 29 | created() { 30 | this.fetchRest(); 31 | }, 32 | methods: { 33 | async fetchRest() { 34 | const response = await Promise.resolve({ data: "success" }); 35 | this.rest = response.data; 36 | } 37 | } 38 | }); 39 | 40 | localVue.use(VTrack, { 41 | trackEvents 42 | }); 43 | 44 | describe("TrackClickWatch", () => { 45 | it("确保埋点在Promise then之后上报", done => { 46 | const wrapper = mount(TrackClickWatch, { 47 | localVue, 48 | mocks: { 49 | $route 50 | } 51 | }); 52 | 53 | expect(wrapper.vm.rest).toBeNull(); 54 | expect(mockTrackAction).toBeCalledTimes(0); 55 | wrapper.vm.$nextTick(() => { 56 | expect(wrapper.vm.rest).toBe("success"); 57 | expect(mockTrackAction).toBeCalledTimes(1); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/track-show-once.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-12 21:03:46 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 15:01:48 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | import { mockParentNode, mockRect } from "../helper"; 11 | 12 | const localVue = createLocalVue(); 13 | const mockTrackAction = jest.fn(); 14 | const trackEvents = { 15 | 18015: mockTrackAction 16 | }; 17 | const TrackShowOnce = Vue.extend({ 18 | template: ` 19 |
20 | ` 21 | }); 22 | 23 | localVue.use(VTrack, { 24 | trackEvents 25 | }); 26 | 27 | jest.useFakeTimers(); 28 | 29 | describe("TrackShowOnce", () => { 30 | it("确保DOM元素完全可见之后触发埋点,且只触发一次", () => { 31 | const wrapper = mount(TrackShowOnce, { localVue }); 32 | const vm = wrapper.vm; 33 | const scrollEvent = new UIEvent("scroll", { detail: 0 }); 34 | 35 | mockParentNode(vm.$el); 36 | mockRect(vm.$el); 37 | window.dispatchEvent(scrollEvent); 38 | jest.runAllTimers(); 39 | expect(mockTrackAction).toBeCalledTimes(1); 40 | 41 | [1, 2, 3].forEach(() => { 42 | window.dispatchEvent(scrollEvent); 43 | }); 44 | jest.runAllTimers(); 45 | expect(mockTrackAction).toBeCalledTimes(1); 46 | 47 | [1, 2, 3].forEach(async () => { 48 | await setTimeout(() => { 49 | window.dispatchEvent(scrollEvent); 50 | }, 300); 51 | }); 52 | jest.runAllTimers(); 53 | expect(mockTrackAction).toBeCalledTimes(1); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/unit/track-show.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-12 21:03:46 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-08-13 13:59:33 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | import { mockParentNode, mockRect } from "../helper"; 11 | 12 | const localVue = createLocalVue(); 13 | const mockTrackAction = jest.fn(); 14 | const trackEvents = { 15 | 18015: mockTrackAction 16 | }; 17 | const TrackShow = Vue.extend({ 18 | template: ` 19 |
20 | ` 21 | }); 22 | 23 | localVue.use(VTrack, { 24 | trackEvents 25 | }); 26 | 27 | jest.useFakeTimers(); 28 | 29 | describe("TrackShow", () => { 30 | it("确保DOM元素完全可见之后触发埋点,且至少间隔200ms执行一下", () => { 31 | const wrapper = mount(TrackShow, { localVue }); 32 | const vm = wrapper.vm; 33 | const scrollEvent = new UIEvent("scroll", { detail: 0 }); 34 | 35 | mockParentNode(vm.$el); 36 | mockRect(vm.$el); 37 | window.dispatchEvent(scrollEvent); 38 | jest.runAllTimers(); 39 | expect(mockTrackAction).toBeCalledTimes(1); 40 | 41 | // [1, 2, 3].forEach(() => { 42 | // window.dispatchEvent(scrollEvent); 43 | // }); 44 | // jest.runAllTimers(); 45 | // expect(mockTrackAction).toBeCalledTimes(2); 46 | 47 | // [1, 2, 3].forEach(async () => { 48 | // await setTimeout(() => { 49 | // window.dispatchEvent(scrollEvent2); 50 | // }, 300); 51 | // }); 52 | // jest.runAllTimers(); 53 | // expect(mockTrackAction).toBeCalledTimes(5); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/unit/track-custom-event-async.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-13 22:00:04 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 22:54:09 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(({ status }) => status); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const Child = Vue.extend({ 17 | template: ` 18 |
19 | ` 20 | }); 21 | const TrackCustomEventAsync = Vue.extend({ 22 | template: ` 23 | 24 | `, 25 | components: { 26 | child: Child 27 | }, 28 | data() { 29 | return { 30 | rest: null 31 | }; 32 | }, 33 | methods: { 34 | async fetchRest() { 35 | const response = await Promise.resolve({ data: "success" }); 36 | this.rest = response.data; 37 | } 38 | } 39 | }); 40 | 41 | localVue.use(VTrack, { 42 | trackEvents 43 | }); 44 | 45 | describe("TrackCustomEventAsync", () => { 46 | it("确保异步的自定义事件返回成功之后再上报埋点", done => { 47 | const wrapper = mount(TrackCustomEventAsync, { 48 | localVue 49 | }); 50 | const vm = wrapper.vm; 51 | 52 | wrapper.find(Child).trigger("click"); 53 | expect(vm.rest).toBeNull(); 54 | expect(mockTrackAction).toBeCalledTimes(0); 55 | vm.$nextTick(() => { 56 | expect(vm.rest).toBe("success"); 57 | expect(mockTrackAction).toBeCalledTimes(1); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/track-view-watch-delay.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-10 22:56:35 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-10 23:09:38 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const $route = { 17 | name: "HOME", 18 | path: "/home" 19 | }; 20 | const TrackViewWatchDelay = Vue.extend({ 21 | template: ` 22 | 23 | `, 24 | data() { 25 | return { 26 | rest: null 27 | }; 28 | }, 29 | created() { 30 | this.fetchRest(); 31 | }, 32 | methods: { 33 | async fetchRest() { 34 | const response = await Promise.resolve({ data: "success" }); 35 | this.rest = response.data; 36 | } 37 | } 38 | }); 39 | 40 | localVue.use(VTrack, { 41 | trackEvents 42 | }); 43 | 44 | describe("TrackViewWatchDelay", () => { 45 | it("确保埋点在Promise then之后并且延迟1000ms之后上报", done => { 46 | const wrapper = mount(TrackViewWatchDelay, { 47 | localVue, 48 | mocks: { 49 | $route 50 | } 51 | }); 52 | 53 | expect(wrapper.vm.rest).toBeNull(); 54 | wrapper.vm.$nextTick(() => { 55 | expect(wrapper.vm.rest).toBe("success"); 56 | expect(mockTrackAction).toBeCalledTimes(0); 57 | setTimeout(() => { 58 | expect(mockTrackAction).toBeCalledTimes(1); 59 | }, 1000); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/unit/track-custom-event-param.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-13 21:03:55 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-16 22:33:20 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | 11 | const localVue = createLocalVue(); 12 | const mockTrackAction = jest.fn(); 13 | const trackEvents = { 14 | 18015: mockTrackAction 15 | }; 16 | const Child = Vue.extend({ 17 | template: ` 18 |
19 | `, 20 | data() { 21 | return { 22 | item: { 23 | id: 0 24 | } 25 | }; 26 | } 27 | }); 28 | const TrackCustomEventParam = Vue.extend({ 29 | template: ` 30 | 31 | `, 32 | components: { 33 | child: Child 34 | }, 35 | data() { 36 | return { 37 | id: 0 38 | }; 39 | }, 40 | methods: { 41 | handleClick({ id }) { 42 | this.id = id; 43 | } 44 | } 45 | }); 46 | 47 | localVue.use(VTrack, { 48 | trackEvents 49 | }); 50 | 51 | describe("TrackCustomEventParam", () => { 52 | it("确保自定义事件传参正确,并且点击事件次数等于埋点上报次数", () => { 53 | const wrapper = mount(TrackCustomEventParam, { 54 | localVue 55 | }); 56 | const vm = wrapper.vm; 57 | const childWrapper = wrapper.find(Child); 58 | 59 | expect(vm.id).toBe(0); 60 | childWrapper.setData({ item: { id: 1 } }); 61 | childWrapper.trigger("click"); 62 | expect(vm.id).toBe(1); 63 | childWrapper.setData({ item: { id: 2 } }); 64 | childWrapper.trigger("click"); 65 | expect(vm.id).toBe(2); 66 | expect(mockTrackAction).toBeCalledTimes(2); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/unit/track-show-custom-scroll-once.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-12 21:03:46 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-04-13 22:58:00 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | import { mockParentNode, mockRect } from "../helper"; 11 | 12 | const localVue = createLocalVue(); 13 | const mockTrackAction = jest.fn(); 14 | const trackEvents = { 15 | 18015: mockTrackAction 16 | }; 17 | const Cmp = Vue.extend({ 18 | template: `
` 19 | }); 20 | const CmpTrackShowCustomOnce = Vue.extend({ 21 | template: ` 22 | 23 | 24 | 25 | `, 26 | components: { 27 | cmp: Cmp 28 | } 29 | }); 30 | 31 | localVue.use(VTrack, { 32 | trackEvents 33 | }); 34 | 35 | jest.useFakeTimers(); 36 | 37 | describe("CmpTrackShowCustomOnce", () => { 38 | it("确保组件完全可见之后触发埋点,且只触发一次", () => { 39 | const wrapper = mount(CmpTrackShowCustomOnce, { localVue }); 40 | const vm = wrapper.find(Cmp).vm; 41 | 42 | mockParentNode(vm.$el); 43 | mockRect(vm.$el); 44 | vm.$emit("scroll"); 45 | jest.runAllTimers(); 46 | expect(mockTrackAction).toBeCalledTimes(1); 47 | 48 | [1, 2, 3].forEach(() => { 49 | vm.$emit("scroll"); 50 | }); 51 | jest.runAllTimers(); 52 | expect(mockTrackAction).toBeCalledTimes(1); 53 | 54 | [1, 2, 3].forEach(async () => { 55 | await setTimeout(() => { 56 | vm.$emit("scroll"); 57 | }, 300); 58 | }); 59 | jest.runAllTimers(); 60 | expect(mockTrackAction).toBeCalledTimes(1); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/track-show-custom-scroll.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-12 21:03:46 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-08-13 14:00:05 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import { mount, createLocalVue } from "@vue/test-utils"; 10 | import { mockParentNode, mockRect } from "../helper"; 11 | 12 | const localVue = createLocalVue(); 13 | const mockTrackAction = jest.fn(); 14 | const trackEvents = { 15 | 18015: mockTrackAction 16 | }; 17 | const Cmp = Vue.extend({ 18 | template: `
` 19 | }); 20 | const TrackShowCustomScroll = Vue.extend({ 21 | template: ` 22 | 23 | 24 | 25 | `, 26 | components: { 27 | cmp: Cmp 28 | } 29 | }); 30 | 31 | localVue.use(VTrack, { 32 | trackEvents 33 | }); 34 | 35 | jest.useFakeTimers(); 36 | 37 | describe("TrackShowCustomScroll", () => { 38 | it("确保组件完全可见之后触发埋点,且至少间隔200ms执行一下", () => { 39 | const wrapper = mount(TrackShowCustomScroll, { localVue }); 40 | const vm = wrapper.find(Cmp).vm; 41 | 42 | mockParentNode(vm.$el); 43 | mockRect(vm.$el); 44 | vm.$emit("scroll"); 45 | jest.runAllTimers(); 46 | expect(mockTrackAction).toBeCalledTimes(1); 47 | 48 | // [1, 2, 3].forEach(() => { 49 | // vm.$emit("scroll"); 50 | // }); 51 | // jest.runAllTimers(); 52 | // expect(mockTrackAction).toBeCalledTimes(2); 53 | 54 | // [1, 2, 3].forEach(async () => { 55 | // await setTimeout(() => { 56 | // vm.$emit("scroll"); 57 | // }, 300); 58 | // }); 59 | // jest.runAllTimers(); 60 | // expect(mockTrackAction).toBeCalledTimes(5); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /docs/components/code-snippet.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 37 | 38 | 73 | -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import { 4 | Alert, 5 | Card, 6 | Collapse, 7 | CollapseItem, 8 | Message, 9 | Notification 10 | } from "element-ui"; 11 | import App from "./App.vue"; 12 | import Home from "./pages/home.vue"; 13 | import Started from "./pages/started.vue"; 14 | import CustomEvents from "./pages/custom-events.vue"; 15 | import TrackView from "./pages/track-view.vue"; 16 | import BlockShow from "./pages/block-show.vue"; 17 | import VueTrack from "../"; 18 | import trackEvents from "./tracks"; 19 | 20 | Vue.use(VueRouter); 21 | Vue.use(Alert); 22 | Vue.use(Card); 23 | Vue.use(Collapse); 24 | Vue.use(CollapseItem); 25 | Vue.use(VueTrack, { 26 | trackEvents, 27 | trackEnable: { 28 | UVPV: "routeUpdate", 29 | TONP: true 30 | } 31 | }); 32 | 33 | Vue.prototype.$message = Message; 34 | Vue.prototype.$notify = Notification; 35 | 36 | const router = new VueRouter({ 37 | routes: [ 38 | { 39 | path: "/", 40 | name: "HOME", 41 | component: Home 42 | }, 43 | { 44 | path: "/started", 45 | name: "STARTED", 46 | component: Started 47 | }, 48 | { 49 | path: "/custom-events", 50 | name: "CUSTOM_EVENTS", 51 | component: CustomEvents 52 | }, 53 | { 54 | path: "/track-view", 55 | name: "TRACK_VIEW", 56 | component: TrackView, 57 | meta: { 58 | keepAlive: true 59 | } 60 | }, 61 | { 62 | path: "/block-show", 63 | name: "BLOCK_SHOW", 64 | component: BlockShow 65 | }, 66 | { 67 | path: "*", 68 | redirect: "/" 69 | } 70 | ] 71 | }); 72 | 73 | /* eslint-disable no-new */ 74 | new Vue({ 75 | el: "#app", 76 | router, 77 | render: h => h(App) 78 | }); 79 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-08 11:13:34 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2020-06-04 17:08:44 6 | */ 7 | 8 | /** 9 | * @desc 是否为元素几点 10 | * 11 | * @param {DOMElement} ele 一个 DOM 元素 12 | * @return {Boolean} 13 | */ 14 | export const isElement = ele => ele && ele.nodeType === 1; 15 | 16 | /** 17 | * @desc 获取 DOM CSS 属性的值 18 | * 19 | * @param {DOMElement} ele A DOM 元素 20 | * @returns {String} 21 | */ 22 | export function getStylePropValue(ele, prop) { 23 | return window.getComputedStyle(ele).getPropertyValue(prop); 24 | } 25 | 26 | /** 27 | * @desc 元素是否在可视区域可见 28 | * 29 | * @param {Object} rect 元素大小及相对可视区域的位置信息 30 | * @returns {Boolean} true => 可见 false => 不可见 31 | */ 32 | export function isInViewport(rect, viewport) { 33 | if (!rect || (rect.width <= 0 || rect.height <= 0)) { 34 | return false; 35 | } 36 | 37 | return ( 38 | rect.bottom > 0 && 39 | rect.right > 0 && 40 | rect.top < window.innerHeight && 41 | rect.left < window.innerWidth && 42 | !( 43 | rect.left > viewport.right || 44 | rect.top > viewport.bottom || 45 | rect.right < viewport.left || 46 | rect.bottom < viewport.top 47 | ) 48 | ); 49 | } 50 | 51 | /** 52 | * @desc 元素是否隐藏 53 | * 54 | * @param {DOMElement} ele A DOM 元素 55 | * @returns {Boolean} true => 未隐藏可见 false => 隐藏不可见 56 | */ 57 | export function isVisible(ele) { 58 | if (ele === window.document) { 59 | return true; 60 | } 61 | if (!ele || !ele.parentNode) { 62 | return false; 63 | } 64 | 65 | const parent = ele.parentNode; 66 | const visibility = getStylePropValue(ele, "visibility"); 67 | const display = getStylePropValue(ele, "display"); 68 | 69 | if (visibility === "hidden" || display === "none") { 70 | return false; 71 | } 72 | return parent ? isVisible(parent) : true; 73 | } 74 | -------------------------------------------------------------------------------- /tests/unit/track-click-native.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-05-28 17:46:45 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-05-28 18:37:50 6 | */ 7 | import Vue from "vue"; 8 | import VTrack from "@/"; 9 | import VueRouter from "vue-router"; 10 | import { mount, createLocalVue } from "@vue/test-utils"; 11 | 12 | const localVue = createLocalVue(); 13 | const mockTrackAction = jest.fn(); 14 | const trackEvents = { 15 | 18015: mockTrackAction 16 | }; 17 | const Button = Vue.extend({ 18 | template: ` 19 |
20 | ` 21 | }); 22 | const TrackClickNative = Vue.extend({ 23 | template: ` 24 | 25 | `, 26 | components: { 27 | Button 28 | }, 29 | data() { 30 | return { 31 | count: 0 32 | }; 33 | }, 34 | methods: { 35 | handleNativeEvent() { 36 | this.count += 1; 37 | } 38 | } 39 | }); 40 | const RouterLinkTrackClickNative = Vue.extend({ 41 | template: ` 42 | 43 | `, 44 | data() { 45 | return { 46 | count: 0 47 | }; 48 | }, 49 | methods: { 50 | handleNativeEvent() { 51 | this.count += 1; 52 | } 53 | } 54 | }); 55 | 56 | localVue.use(VueRouter); 57 | localVue.use(VTrack, { 58 | trackEvents 59 | }); 60 | 61 | describe("TrackClickNative", () => { 62 | test("确保组件原生click事件、埋点事件正常触发,且埋点上报次数为1", () => { 63 | const wrapper = mount(TrackClickNative, { 64 | localVue 65 | }); 66 | 67 | expect(wrapper.vm.count).toBe(0); 68 | wrapper.trigger("click"); 69 | expect(wrapper.vm.count).toBe(1); 70 | expect(mockTrackAction).toBeCalledTimes(1); 71 | }); 72 | 73 | test("确保组件原生click事件、埋点事件正常触发,且埋点上报次数为2", () => { 74 | const wrapper = mount(RouterLinkTrackClickNative, { 75 | localVue, 76 | stubs: ["router-link"] 77 | }); 78 | 79 | expect(wrapper.vm.count).toBe(0); 80 | wrapper.trigger("click"); 81 | expect(wrapper.vm.count).toBe(1); 82 | expect(mockTrackAction).toBeCalledTimes(2); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-03-06 17:49:29 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2020-01-13 14:38:02 6 | */ 7 | import * as hooks from "./hooks"; 8 | 9 | export default class VTrack { 10 | constructor() { 11 | this.installed = false; 12 | } 13 | // 保存当前点击的元素 14 | static target = null; 15 | // Vue.use 将执行此方法 16 | static install(Vue, { trackEvents, trackEnable = {} }) { 17 | trackEnable = { 18 | UVPV: false, 19 | TONP: false, 20 | ...trackEnable 21 | }; 22 | const TRACK_TONP = (ctx, et) => { 23 | if (trackEnable.TONP) { 24 | trackEvents.TONP(ctx, { 25 | et, 26 | dt: Date.now() 27 | }); 28 | } 29 | }; 30 | 31 | if (this.installed) return; 32 | this.installed = true; 33 | 34 | // 注册v-track全局指令 35 | Vue.directive("track", { 36 | bind: (...args) => hooks.bind.call(this, ...args, trackEvents), 37 | componentUpdated: (...args) => 38 | hooks.updated.call(this, ...args, trackEvents), 39 | unbind: (...args) => hooks.unbind.call(this, ...args) 40 | }); 41 | 42 | // 注册全局组件 43 | Vue.component("TrackView", { 44 | render: h => 45 | h("span", { 46 | style: "display: none" 47 | }) 48 | }); 49 | 50 | Vue.mixin({ 51 | data: () => ({ 52 | PAGE_ENTER_TIME: Date.now() 53 | }), 54 | created() { 55 | window.onbeforeunload = () => TRACK_TONP(this, this.PAGE_ENTER_TIME); 56 | }, 57 | // 统计UV、PV 58 | beforeRouteEnter(_, __, next) { 59 | next(vm => { 60 | trackEnable.UVPV && trackEvents.UVPV(vm); 61 | }); 62 | }, 63 | beforeRouteUpdate(_, __, next) { 64 | // 确保导航升级完成 65 | this.$watch("$route", () => { 66 | if (trackEnable.UVPV && trackEnable.UVPV === "routeUpdate") { 67 | trackEvents.UVPV(this); 68 | } 69 | }); 70 | next(); 71 | }, 72 | // 页面停留时间 73 | beforeRouteLeave(_, __, next) { 74 | TRACK_TONP(this, this.PAGE_ENTER_TIME); 75 | next(); 76 | } 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /docs/App.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 58 | 59 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v-track", 3 | "version": "0.8.10", 4 | "description": "一个基于Vue指令的埋点插件", 5 | "author": "LHammer ", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "dev": "cross-env NODE_ENV=development rollup --config build/rollup.config.es.js --watch", 9 | "build": "NODE_ENV=production npm run build:es && npm run build:browser", 10 | "build:browser": "rollup --config build/rollup.config.browser.js && npm run size", 11 | "build:es": "rollup --config build/rollup.config.es.js", 12 | "build:docs": "vue-cli-service build", 13 | "test": "npm run lint && npm run test:unit", 14 | "lint": "vue-cli-service lint", 15 | "prepublishOnly": "npm run test && npm run build", 16 | "size": "gzip-size dist/v-track.min.js", 17 | "test:unit": "vue-cli-service test:unit" 18 | }, 19 | "main": "dist/v-track.min.js", 20 | "module": "dist/v-track.esm.js", 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "@babel/core": "^7.4.3", 24 | "@babel/plugin-proposal-class-properties": "^7.4.0", 25 | "@babel/preset-env": "^7.4.3", 26 | "@vue/cli-plugin-babel": "^3.5.0", 27 | "@vue/cli-plugin-eslint": "^3.5.0", 28 | "@vue/cli-plugin-unit-jest": "^3.5.3", 29 | "@vue/cli-service": "^3.5.0", 30 | "@vue/eslint-config-prettier": "^4.0.1", 31 | "@vue/test-utils": "1.0.0-beta.29", 32 | "babel-core": "7.0.0-bridge.0", 33 | "babel-eslint": "^10.0.1", 34 | "babel-jest": "^23.6.0", 35 | "babel-plugin-component": "^1.1.1", 36 | "cross-env": "^5.2.0", 37 | "element-ui": "^2.9.1", 38 | "eslint": "^5.8.0", 39 | "eslint-plugin-vue": "^5.0.0", 40 | "gzip-size-cli": "^3.0.0", 41 | "highlight.js": "^9.15.6", 42 | "rollup": "^1.9.0", 43 | "rollup-plugin-babel": "^4.3.2", 44 | "rollup-plugin-commonjs": "^9.3.4", 45 | "rollup-plugin-node-resolve": "^4.2.1", 46 | "rollup-plugin-replace": "^2.2.0", 47 | "rollup-plugin-uglify": "^6.0.2", 48 | "sass": "^1.18.0", 49 | "sass-loader": "^7.1.0", 50 | "vue": "^2.6.10", 51 | "vue-router": "^3.0.4", 52 | "vue-template-compiler": "^2.5.21" 53 | }, 54 | "keywords": [ 55 | "vue", 56 | "javascript", 57 | "track", 58 | "directive", 59 | "plugin", 60 | "vue-components" 61 | ], 62 | "license": "MIT", 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/l-hammer/v-track.git" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-08 11:13:34 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2019-08-05 15:31:00 6 | */ 7 | 8 | /** 9 | * @desc 判断给定变量是否为一个函数 10 | * 11 | * @param {*} v 12 | * @return {Boolean} 13 | */ 14 | export const isFun = v => typeof v === "function" || false; 15 | 16 | /** 17 | * @desc 判断给定变量是否是未定义 18 | * 19 | * @param {*} v 20 | */ 21 | export const isUndef = v => v === undefined || v === null; 22 | 23 | /** 24 | * @desc 判断给定变量是否是定义 25 | * 26 | * @param {*} v 27 | */ 28 | export const isDef = v => v !== undefined && v !== null; 29 | 30 | /** 31 | * @desc 获取对象的键值 32 | * 33 | * @param {Object} value 34 | * @returns {Array} [keys, values] 35 | */ 36 | export function zipArray(value = {}) { 37 | return [Object.values(value), Object.keys(value)]; 38 | } 39 | 40 | /** 41 | * @desc 防抖函数,至少间隔200毫秒执行一次 42 | * 43 | * @param {Function} fn callback 44 | * @param {Number} [ms=200] 默认200毫秒 45 | * @returns {Function} 46 | */ 47 | export function debounce(fn, ms = 200) { 48 | let timeoutId; 49 | return function(...args) { 50 | clearTimeout(timeoutId); 51 | timeoutId = setTimeout(() => fn.apply(this, args), ms); 52 | }; 53 | } 54 | 55 | /** 56 | * @desc 判断给定变量是否完全匹配目标数组 57 | * 58 | * @param {String[]} mdfs 目标数组 59 | * @param {String} vals 60 | * @returns {Boolean} 61 | */ 62 | export function _exactMatch(mdfs, vals) { 63 | const keys = Object.keys(mdfs); 64 | 65 | return keys.length === vals.length && vals.every(v => keys.includes(v)); 66 | } 67 | 68 | /** 69 | * @desc 判断给定变量是否匹配目标数组的一部分 70 | * 71 | * @param {String[]} mdfs 目标字符串数组 72 | * @param {String} vals 73 | * @returns {Boolean} 74 | */ 75 | export function _partialMatch(mdfs, vals) { 76 | const keys = Object.keys(mdfs); 77 | 78 | return vals.some(v => keys.includes(v)); 79 | } 80 | 81 | /** 82 | * @desc 判断两个节点是否为同一个vnode节点 83 | * 84 | * @param {VNode} a 虚拟节点 85 | * @param {VNode} b 虚拟节点 86 | */ 87 | export function sameVnode(a, b) { 88 | return ( 89 | a.key === b.key && 90 | a.tag === b.tag && 91 | a.isComment === b.isComment && 92 | isDef(a.data) === isDef(b.data) 93 | ); 94 | } 95 | 96 | /** 97 | * @desc 判断两个vnode节点是否全等 98 | * 99 | * @param {VNode} a 虚拟节点 100 | * @param {VNode} b 虚拟节点 101 | */ 102 | export function exactlySameVnode(vnode, oldVnode) { 103 | if (!sameVnode(vnode, oldVnode)) return false; 104 | 105 | const oldCh = oldVnode.children; 106 | const ch = vnode.children; 107 | 108 | // vnode为非文本节点,且新旧节点的子节点都存在但不相同 109 | if (isUndef(vnode.text) && isDef(oldCh) && isDef(ch)) { 110 | if (oldCh.length !== ch.length) return false; 111 | for (let i = 0; i < ch.length; i++) { 112 | const c = ch[i]; 113 | 114 | if (isDef(c) && isDef(oldCh[i])) { 115 | return exactlySameVnode(c, oldCh[i]); 116 | } 117 | } 118 | } 119 | // vnode为文本节点,新旧节点内容不相同 120 | else if (vnode.text !== oldVnode.text) return false; 121 | return true; 122 | } 123 | -------------------------------------------------------------------------------- /docs/tracks/events.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-14 17:10:31 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2020-06-05 20:42:48 6 | */ 7 | import trackAction from "./action"; 8 | 9 | export default { 10 | /** 11 | * @name UVPV 固定名称不支持修改 12 | * @desc UV、PV埋点 13 | * @param {Object} context 当前上下文 14 | */ 15 | UVPV() { 16 | trackAction("1"); 17 | }, 18 | /** 19 | * @name TONP 固定名称不支持修改 20 | * @desc 页面停留时间埋点(Time on Page) 21 | * @param {Object} context 当前上下文 22 | * @param {Timestamp} et 进入页面时间 23 | * @param {Timestamp} dt 离开页面时间 24 | */ 25 | TONP(_, { et, dt }) { 26 | trackAction("2", { 27 | stt: `${(dt - et) / 1e3}s` 28 | }); 29 | }, 30 | /** 31 | * @desc 测试埋点 32 | */ 33 | 18015({ $route: { name } }) { 34 | trackAction("18015", { 35 | source_page: name // 页面来源 36 | }); 37 | }, 38 | /** 39 | * @param {Object} context 当前上下文 40 | * @param {Object} item 事件参数 41 | * @param {Object} event 事件对象 42 | */ 43 | 18016( 44 | { 45 | $route: { name } 46 | }, 47 | { id }, 48 | { target } 49 | ) { 50 | trackAction("18016", { 51 | id, 52 | source_page: name, 53 | target 54 | }); 55 | }, 56 | 18017({ index, $route: { name } }) { 57 | trackAction("18017", { 58 | source_page: name, 59 | index 60 | }); 61 | }, 62 | 18018({ rest, $route: { name } }) { 63 | trackAction("18018", { 64 | source_page: name, 65 | rest 66 | }); 67 | }, 68 | 18019( 69 | { 70 | $route: { name } 71 | }, 72 | { id }, 73 | { target } 74 | ) { 75 | trackAction("18019", { 76 | id, 77 | source_page: name, 78 | target 79 | }); 80 | }, 81 | 18020({ activeName, $route: { name } }) { 82 | trackAction("18020", { 83 | source_page: name, 84 | active_name: activeName 85 | }); 86 | }, 87 | 18021({ rest, $route: { name } }) { 88 | trackAction("18021", { 89 | source_page: name, 90 | rest 91 | }); 92 | }, 93 | 18022({ $route: { name } }) { 94 | trackAction("18022", { 95 | source_page: name, 96 | description: "这是一个页面初始化埋点" 97 | }); 98 | }, 99 | 18023({ $route: { name } }) { 100 | trackAction("18023", { 101 | source_page: name 102 | }); 103 | }, 104 | 18025({ rest1, $route: { name } }) { 105 | trackAction("18025", { 106 | source_page: name, 107 | rest: rest1 108 | }); 109 | }, 110 | 18026({ rest2, $route: { name } }) { 111 | trackAction("18026", { 112 | source_page: name, 113 | rest: rest2 114 | }); 115 | }, 116 | 18027({ $route: { name } }) { 117 | trackAction("18027", { 118 | source_page: name, 119 | description: "我想被曝光无数次" 120 | }); 121 | }, 122 | 18028({ $route: { name } }) { 123 | trackAction("18028", { 124 | source_page: name, 125 | description: "我只想被曝光一次" 126 | }); 127 | }, 128 | 18029({ $route: { name } }) { 129 | trackAction("18029", { 130 | source_page: name, 131 | description: "我也想被曝光无数次" 132 | }); 133 | }, 134 | 18030({ $route: { name } }) { 135 | trackAction("18030", { 136 | source_page: name, 137 | description: "我也想被曝光无数次" 138 | }); 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /docs/assets/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: white; 3 | margin: 0; 4 | font-family: "Avenir", Helvetica, Arial, sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | header { 9 | background: $primary-color; 10 | padding: 32px; 11 | padding-bottom: 8px; 12 | nav, 13 | .description { 14 | text-align: center; 15 | } 16 | .description { 17 | color: white; 18 | margin-top: 15px; 19 | } 20 | } 21 | section { 22 | .section-content { 23 | margin: 0 42px; 24 | padding: 64px 0; 25 | box-sizing: border-box; 26 | &.small { 27 | padding: 32px 0; 28 | } 29 | &.mini { 30 | padding: 16px 0; 31 | } 32 | &.large { 33 | padding: 152px 0; 34 | } 35 | } 36 | &.nav { 37 | text-align: center; 38 | background: $primary-color; 39 | padding: 24px; 40 | padding-bottom: 35px; 41 | @include h-box; 42 | @include box-center; 43 | a { 44 | display: inline-block; 45 | padding: 8px 16px; 46 | color: white; 47 | background: lighten($primary-color, 10%); 48 | border-radius: 3px; 49 | &:hover { 50 | background: lighten($primary-color, 20%); 51 | } 52 | &:not(:last-child) { 53 | margin-right: 8px; 54 | } 55 | } 56 | } 57 | } 58 | 59 | .collapse { 60 | .section-content { 61 | padding: 12px 0 64px 0; 62 | } 63 | } 64 | 65 | h1 { 66 | color: white; 67 | text-align: center; 68 | margin: 0 0 32px; 69 | } 70 | 71 | a { 72 | color: $primary-color; 73 | text-decoration: none; 74 | cursor: pointer; 75 | &:hover { 76 | color: lighten($primary-color, 10%); 77 | } 78 | } 79 | 80 | .command { 81 | background: darken($primary-color, 10%); 82 | color: white; 83 | font-family: monospace; 84 | max-width: 500px; 85 | margin: 20px auto; 86 | border-radius: 2px; 87 | padding: 12px 24px; 88 | box-sizing: border-box; 89 | text-align: center; 90 | } 91 | .plus { 92 | text-align: center; 93 | color: $primary-color; 94 | font-size: 32px; 95 | margin: 12px; 96 | } 97 | 98 | .demo, 99 | .snippets { 100 | max-width: 1200px; 101 | margin: auto; 102 | } 103 | 104 | .snippet + .snippet { 105 | margin-top: 20px; 106 | } 107 | 108 | .snippet:last-child { 109 | margin-bottom: 39px; 110 | } 111 | 112 | .snippets { 113 | background: #f9f9f9; 114 | border-radius: 0 0 3px 3px; 115 | } 116 | 117 | .demo { 118 | background: #f9f9f9; 119 | margin-top: 42px; 120 | border-radius: 3px 3px 0 0; 121 | overflow: hidden; 122 | .section-content { 123 | text-align: center; 124 | max-width: 560px; 125 | margin: auto; 126 | } 127 | } 128 | 129 | .demo.viewport { 130 | height: 364px; 131 | overflow: auto; 132 | 133 | .section-content.large { 134 | padding: 375px 0; 135 | } 136 | } 137 | 138 | .footer { 139 | font-size: 14px; 140 | text-align: center; 141 | background: lighten($primary-color, 45%); 142 | } 143 | 144 | .track-button { 145 | width: 229px; 146 | height: 39px; 147 | background: $primary-color; 148 | border-radius: 5px; 149 | color: white; 150 | line-height: 39px; 151 | margin: auto; 152 | cursor: pointer; 153 | user-select: none; 154 | &:hover { 155 | background: lighten($primary-color, 5%); 156 | } 157 | &:active { 158 | background: darken($primary-color, 5%); 159 | } 160 | } 161 | 162 | .v-track-table { 163 | .header, 164 | .row { 165 | display: flex; 166 | strong, 167 | span { 168 | flex: 1; 169 | line-height: 29px; 170 | border-bottom: 1px solid #ebeef5; 171 | } 172 | } 173 | .row:hover { 174 | background: #fafafa; 175 | cursor: pointer; 176 | } 177 | } 178 | 179 | .notification__large { 180 | width: 399px; 181 | .el-notification__group { 182 | width: 100%; 183 | } 184 | } 185 | 186 | .message-offset { 187 | top: 88px; 188 | } 189 | 190 | .section-content.collapse-wrapper { 191 | padding: 20px; 192 | margin: 20px auto; 193 | background: white; 194 | max-width: 690px; 195 | text-align: left; 196 | box-shadow: 0 2px 4px rgba(232, 237, 250, 0.9); 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v-track 2 | 3 | Travis (.org) branch 4 | Gzip Size 5 | 6 | 7 | 8 | 9 | 10 | v-track通过 Vue [自定义指令](https://cn.vuejs.org/v2/guide/custom-directive.html)的方式将埋点代码与业务代码完全解耦~ 11 | 12 | ## 安装 13 | 14 | ### YARN 15 | 16 | ```shell 17 | $ yarn add v-track 18 | ``` 19 | 20 | ### NPM 21 | 22 | ```shell 23 | $ npm install v-track --save 24 | ``` 25 | 26 | ### CDN 27 | 28 | 目前可以通过[unpkg.com/v-track](https://unpkg.com/v-track/)获取到最新版本的资源,在页面上使用 script 标签直接引入文件即可开始使用 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | 或者 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | > 建议使用 CDN 引入 v-track 的用户在链接地址上锁定版本,以免将来 v-track 升级时受到非兼容性更新的影响。锁定版本的方法请查看 [unpkg.com](https://unpkg.com/) or [jsdelivr.com](https://www.jsdelivr.com/)。 41 | 42 | ## 用法 43 | 44 | ```js 45 | import Vue from "vue" 46 | import VTrack from "v-track" 47 | import trackEvents from "./track-events" 48 | 49 | Vue.use(VTrack, { 50 | trackEvents, // 埋点事件对象 51 | trackEnable: { 52 | UVPV: true, // 是否开启UVPV统计,v0.8.3新增routeUpdate,即在当前路由参数发生改变时埋点,默认为false 53 | TONP: true // 是否开启页面停留时长统计,默认为false 54 | } 55 | }) 56 | ``` 57 | 58 | ```js 59 | /* track-events.js */ 60 | export default { 61 | /** 62 | * @name UVPV 固定名称不支持修改 63 | * @desc UV、PV埋点 64 | * @param {Object} context 当前上下文 65 | */ 66 | UVPV(context) { 67 | ... 68 | }, 69 | /** 70 | * @name TONP 固定名称不支持修改 71 | * @desc 页面停留时间埋点(Time on Page) 72 | * @param {Object} context 当前上下文 73 | * @param {Timestamp} et 进入页面时间 74 | * @param {Timestamp} dt 离开页面时间 75 | */ 76 | TONP(context, { et, dt }) { 77 | ... 78 | }, 79 | /** 80 | * @name 18015 埋点唯一标识ID(自定义) 81 | * @param {Object} context 当前上下文 82 | * @param {Object} args 剩余参数 83 | */ 84 | 18015(context, args) { 85 | ... 86 | } 87 | ... 88 | } 89 | ``` 90 | 91 | ```HTML 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 |
101 |
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ``` 116 | 117 | ## 指令修饰符 118 | 119 | - `.click` 表示事件行为的埋点 120 | - `.[custom-event]` 表示自定义事件行为的埋点 121 | - `.native` 表示监听组件原生click事件行为的埋点 122 | - `.watch` 表示页面异步行为的埋点 123 | - `.async` 配合`.click`指令,表示异步事件行为的埋点 124 | - `.delay` 表示埋点是否延迟执行,默认先执行埋点再执行回调 125 | - `.show` 表示区域曝光埋点 126 | - `.once` 配合`.show`指令,只执行一次埋点 127 | - `.custom` 配合`.show`指令,表示使用自定义scroll事件 128 | 129 | ## LICENSE 130 | 131 | [MIT](https://github.com/l-hammer/v-track/blob/master/LICENSE) © 2019-present, LHammer 132 | -------------------------------------------------------------------------------- /docs/pages/started.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 152 | -------------------------------------------------------------------------------- /src/utils/vis-monitor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-04-08 11:13:34 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2020-06-07 10:47:44 6 | */ 7 | import { isElement, isVisible, isInViewport } from "./dom"; 8 | import { isFun, debounce } from "./helper"; 9 | 10 | /** 11 | * @class 12 | * @name VisMonitor 13 | * 14 | * @desc 目标元素控制器 15 | */ 16 | export default class VisMonitor { 17 | constructor(ele, ref, refwin = window, percent = 1) { 18 | if (!isElement(ele)) { 19 | throw new Error("not an element node"); 20 | } 21 | if (percent > 1 && percent <= 0) { 22 | throw new Error("'percent' must be a number between 0 and 1"); 23 | } 24 | this.ele = ele; 25 | this.ref = ref; 26 | this.refWin = refwin; 27 | this.started = false; 28 | this.percent = percent; 29 | this.prevPerc = null; // 保存前一次曝光百分比 30 | this.listeners = {}; 31 | this.removeScrollLisener = null; 32 | this.init(); 33 | } 34 | 35 | init() { 36 | if (!this.started) { 37 | const listener = debounce(this.visibilitychange.bind(this)); 38 | 39 | listener(); 40 | this.removeScrollLisener = (listener => { 41 | if (this.ref) { 42 | return this.ref.$on("scroll", listener); 43 | } else { 44 | this.refWin.addEventListener("scroll", listener, true); 45 | return () => 46 | this.refWin.removeEventListener("scroll", listener, true); 47 | } 48 | })(listener); 49 | this.started = true; 50 | } 51 | } 52 | 53 | viewport() { 54 | const win = this.refWin; 55 | const rect = isElement(win) ? win.getBoundingClientRect() : win; 56 | 57 | return { 58 | top: isElement(win) ? rect.top : 0, 59 | right: rect.right || rect.innerWidth, 60 | bottom: rect.bottom || rect.innerHeight, 61 | left: rect.left || 0, 62 | height: win.innerHeight || win.offsetHeight, 63 | width: win.innerWidth || win.offsetWidth 64 | }; 65 | } 66 | 67 | /** 68 | * 监听自定义事件 69 | */ 70 | $on(evt, cbk) { 71 | const queue = this.listeners[evt] || (this.listeners[evt] = []); 72 | 73 | queue.push(cbk); 74 | return this; 75 | } 76 | 77 | /** 78 | * 移除监听自定义事件 79 | */ 80 | $off(evt, cbk) { 81 | if (!cbk) return; 82 | 83 | let queue = this.listeners[evt]; 84 | let v; 85 | let i = queue.length; 86 | 87 | while (i--) { 88 | v = queue[i]; 89 | if (v === cbk || v.cbk === cbk) { 90 | queue.splice(i, 1); 91 | break; 92 | } 93 | } 94 | return this; 95 | } 96 | 97 | /** 98 | * 监听自定义事件,但只触发一次 99 | */ 100 | $once(evt, cbk) { 101 | const on = (...args) => { 102 | this.$off(evt, on); 103 | cbk.apply(this, args); 104 | }; 105 | 106 | on.cbk = cbk; 107 | this.$on(evt, on); 108 | return this; 109 | } 110 | 111 | /** 112 | * 触发当前实例的监听回调 113 | */ 114 | $emit(evt, ...args) { 115 | const queue = this.listeners[evt] || []; 116 | 117 | queue.forEach(sub => sub.apply(this, args)); 118 | return this; 119 | } 120 | 121 | /** 122 | * 计算元素可见比例,如果比例为100%,则触发 fullyvisible 事件 123 | */ 124 | visibilitychange() { 125 | const rect = this.ele.getBoundingClientRect(); 126 | const view = this.viewport(); 127 | 128 | if (!isInViewport(rect, view) || !isVisible(this.ele)) { 129 | this.prevPerc = 0; 130 | return 0; 131 | } 132 | 133 | let vh = 0; 134 | let vw = 0; 135 | let perc = 0; 136 | 137 | if (view.top < 0) { 138 | view.top = 0; 139 | } 140 | 141 | if (view.bottom > window.innerHeight) { 142 | view.bottom = window.innerHeight; 143 | } 144 | 145 | if (view.left < 0) { 146 | view.left = 0; 147 | } 148 | 149 | if (view.right > window.innerWidth) { 150 | view.right = window.innerWidth; 151 | } 152 | 153 | if (rect.top >= view.top && rect.bottom > view.bottom) { 154 | vh = view.bottom - rect.top; 155 | } else if (rect.top < view.top && rect.bottom <= view.bottom) { 156 | vh = rect.bottom - view.top; 157 | } else { 158 | vh = rect.height; 159 | } 160 | 161 | if (rect.left >= view.left && rect.right > view.right) { 162 | vw = view.right - rect.left; 163 | } else if (rect.left < view.left && rect.right <= view.right) { 164 | vw = rect.right - view.left; 165 | } else { 166 | vw = rect.width; 167 | } 168 | 169 | perc = (vh * vw) / (rect.height * rect.width); 170 | 171 | if (this.prevPerc < this.percent && perc >= this.percent) { 172 | this.$emit("fullyvisible"); 173 | this.prevPerc = perc; 174 | } 175 | } 176 | 177 | /** 178 | * 销毁当前实例的事件 179 | */ 180 | destroy() { 181 | isFun(this.removeScrollLisener) && this.removeScrollLisener(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /docs/pages/block-show.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 178 | -------------------------------------------------------------------------------- /docs/dist/css/app.2e14d2d6.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 4 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}body{background:#fff;margin:0;font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}header{background:#40b883;padding:32px;padding-bottom:8px}header .description,header nav{text-align:center}header .description{color:#fff;margin-top:15px}section .section-content{margin:0 42px;padding:64px 0;-webkit-box-sizing:border-box;box-sizing:border-box}section .section-content.small{padding:32px 0}section .section-content.mini{padding:16px 0}section .section-content.large{padding:152px 0}section.nav{text-align:center;background:#40b883;padding:24px;padding-bottom:35px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}section.nav>*{-webkit-box-flex:1;-ms-flex:auto 0 0px;flex:auto 0 0}section.nav a{display:inline-block;padding:8px 16px;color:#fff;background:#62c99b;border-radius:3px}section.nav a:hover{background:#88d6b4}section.nav a:not(:last-child){margin-right:8px}.collapse .section-content{padding:12px 0 64px 0}h1{color:#fff;text-align:center;margin:0 0 32px}a{color:#40b883;text-decoration:none;cursor:pointer}a:hover{color:#62c99b}.command{background:#339268;color:#fff;font-family:monospace;max-width:500px;margin:20px auto;border-radius:2px;padding:12px 24px;-webkit-box-sizing:border-box;box-sizing:border-box}.command,.plus{text-align:center}.plus{color:#40b883;font-size:32px;margin:12px}.demo,.snippets{max-width:1200px;margin:auto}.snippet+.snippet{margin-top:20px}.snippet:last-child{margin-bottom:39px}.snippets{border-radius:0 0 3px 3px}.demo,.snippets{background:#f9f9f9}.demo{margin-top:42px;border-radius:3px 3px 0 0;overflow:hidden}.demo .section-content{text-align:center;max-width:560px;margin:auto}.demo.viewport{height:364px;overflow:auto}.demo.viewport .section-content.large{padding:375px 0}.footer{font-size:14px;text-align:center;background:#e7f7f0}.track-button{width:229px;height:39px;background:#40b883;border-radius:5px;color:#fff;line-height:39px;margin:auto;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.track-button:hover{background:#50c28f}.track-button:active{background:#39a576}.v-track-table .header,.v-track-table .row{display:-webkit-box;display:-ms-flexbox;display:flex}.v-track-table .header span,.v-track-table .header strong,.v-track-table .row span,.v-track-table .row strong{-webkit-box-flex:1;-ms-flex:1;flex:1;line-height:29px;border-bottom:1px solid #ebeef5}.v-track-table .row:hover{background:#fafafa;cursor:pointer}.notification__large{width:399px}.notification__large .el-notification__group{width:100%}.message-offset{top:88px}.section-content.collapse-wrapper{padding:20px;margin:20px auto;background:#fff;max-width:690px;text-align:left;-webkit-box-shadow:0 2px 4px rgba(232,237,250,.9);box-shadow:0 2px 4px rgba(232,237,250,.9)}.code-snippet{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;background:#2e3440;border-radius:3px;font-family:Roboto Mono,monospace;font-size:10pt;overflow:auto;position:relative}.code-snippet>*{-webkit-box-flex:1;-ms-flex:auto 0 0px;flex:auto 0 0}.code-snippet .line-numbers,.code-snippet .render{color:#d8dee9;padding:20px;padding-right:0}.code-snippet .line-numbers{background:#2e3440;color:#bdbdbd;border-radius:2px 0 0 2px;line-height:18px}.code-snippet .render{white-space:pre;line-height:18px}.code-snippet .language{position:absolute;top:0;right:0;color:#bdbdbd;padding:4px 4px 6px 6px;border-radius:0 0 0 2px} -------------------------------------------------------------------------------- /docs/utils/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author LHammer 3 | * @desc date parse or format date 4 | * @see https://github.com/l-hammer/YDTemplate/blob/master/src/utils/es6/date.js 5 | */ 6 | const twoDigits = /\d\d?/; 7 | const fourDigits = /\d{4}/; 8 | const token = /d{1,2}|M{1,2}|yy(?:yy)?|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g; 9 | const masks = { 10 | default: "yyyy-MM-dd HH:mm:ss", 11 | date: "yyyy-MM-dd", 12 | datetime: "yyyy-MM-dd HH:mm:ss", 13 | time: "HH:mm:ss", 14 | year: "yyyy", 15 | enDate: "M/d/yy", 16 | cnDate: "yyyy 年 MM 月 dd 日" 17 | }; 18 | 19 | const pad = (val, len) => { 20 | val = String(val); 21 | len = len || 2; 22 | while (val.length < len) { 23 | val = `0${val}`; 24 | } 25 | return val; 26 | }; 27 | 28 | const formatFlags = { 29 | yyyy(dateObj) { 30 | return pad(dateObj.getFullYear(), 4); 31 | }, 32 | yy(dateObj) { 33 | return String(dateObj.getFullYear()).substr(2); 34 | }, 35 | M(dateObj) { 36 | return dateObj.getMonth() + 1; 37 | }, 38 | MM(dateObj) { 39 | return pad(dateObj.getMonth() + 1); 40 | }, 41 | d(dateObj) { 42 | return dateObj.getDate(); 43 | }, 44 | dd(dateObj) { 45 | return pad(dateObj.getDate()); 46 | }, 47 | h(dateObj) { 48 | return dateObj.getHours() % 12 || 12; 49 | }, 50 | hh(dateObj) { 51 | return pad(dateObj.getHours() % 12 || 12); 52 | }, 53 | H(dateObj) { 54 | return dateObj.getHours(); 55 | }, 56 | HH(dateObj) { 57 | return pad(dateObj.getHours()); 58 | }, 59 | m(dateObj) { 60 | return dateObj.getMinutes(); 61 | }, 62 | mm(dateObj) { 63 | return pad(dateObj.getMinutes()); 64 | }, 65 | s(dateObj) { 66 | return dateObj.getSeconds(); 67 | }, 68 | ss(dateObj) { 69 | return pad(dateObj.getSeconds()); 70 | } 71 | }; 72 | 73 | /** 74 | * Format a date 75 | * @method format 76 | * @param {Date|number} dateObj new Date(2018, 2, 9) 77 | * @param {String} mask Format of the date e.g. 'yyyy-MM-dd HH:mm:ss' or 'cnDate' 78 | */ 79 | export const format = (dateObj, mask) => { 80 | if (typeof dateObj === "number") { 81 | dateObj = new Date(dateObj); 82 | } 83 | 84 | if ( 85 | Object.prototype.toString.call(dateObj) !== "[object Date]" || 86 | isNaN(dateObj.getTime()) 87 | ) { 88 | throw new Error("Invalid Date in date.format"); 89 | } 90 | mask = masks[mask] || mask || masks.default; 91 | 92 | // return 不可省略 93 | mask = mask.replace(token, $0 => { 94 | return $0 in formatFlags 95 | ? formatFlags[$0](dateObj) 96 | : $0.slice(1, $0.length - 1); 97 | }); 98 | 99 | return mask; 100 | }; 101 | 102 | const parseFlags = { 103 | yyyy: [ 104 | fourDigits, 105 | (d, v) => { 106 | d.year = v; 107 | } 108 | ], 109 | yy: [ 110 | twoDigits, 111 | (d, v) => { 112 | const da = new Date(); 113 | const cent = +`${da.getFullYear()}`.substr(0, 2); 114 | d.year = `${v > 68 ? cent - 1 : cent}${v}`; 115 | } 116 | ], 117 | M: [ 118 | twoDigits, 119 | (d, v) => { 120 | d.month = v - 1; 121 | } 122 | ], 123 | d: [ 124 | twoDigits, 125 | (d, v) => { 126 | d.day = v; 127 | } 128 | ], 129 | h: [ 130 | twoDigits, 131 | (d, v) => { 132 | d.hour = v; 133 | } 134 | ], 135 | m: [ 136 | twoDigits, 137 | (d, v) => { 138 | d.minute = v; 139 | } 140 | ], 141 | s: [ 142 | twoDigits, 143 | (d, v) => { 144 | d.second = v; 145 | } 146 | ] 147 | }; 148 | parseFlags.MM = parseFlags.M; 149 | parseFlags.dd = parseFlags.d; 150 | parseFlags.hh = parseFlags.h; 151 | parseFlags.H = parseFlags.h; 152 | parseFlags.HH = parseFlags.h; 153 | parseFlags.mm = parseFlags.m; 154 | parseFlags.ss = parseFlags.s; 155 | 156 | /** 157 | * Format a date 158 | * @method parse 159 | * @param {String} dateStr Date String e.g. '2018-02-09 09:29:29' or '2018 年 02 月 09 日' 160 | * @param {String} mask Parse of the format date e.g. 'yyyy-MM-dd HH:mm:ss' or 'cnDate' 161 | * @param {Date} 162 | */ 163 | export const parse = (dateStr, mask) => { 164 | let isVaild = true; 165 | const dateInfo = {}; 166 | const today = new Date(); 167 | 168 | if (typeof dateStr !== "string") { 169 | throw new Error("Invalid format in fecha.parse"); 170 | } 171 | 172 | mask = masks[mask] || mask || masks.default; 173 | /** 174 | * @function replace @see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace; 175 | * @param {String} $0 匹配的子串 176 | */ 177 | mask.replace(token, function($0) { 178 | if (parseFlags[$0]) { 179 | const flag = parseFlags[$0]; 180 | /** 181 | * 搜索匹配到子串(e.g. yyyy)对应flag(fourDigits)的位置 182 | * @function search 未匹配到时返回-1,即按位取反为0时表示没有对应的flag 183 | */ 184 | const index = dateStr.search(flag[0]); 185 | if (!~index) { 186 | isVaild = false; 187 | } else { 188 | /** 189 | * 为避免重复返回,将已经返回的值result从dateStr中删除 190 | */ 191 | dateStr.replace(flag[0], function(result) { 192 | flag[1](dateInfo, result); 193 | dateStr = dateStr.substr(index + result.length); 194 | return result; 195 | }); 196 | } 197 | } 198 | return parseFlags[$0] ? "" : $0.slice(1, $0.length - 1); 199 | }); 200 | 201 | if (!isVaild) { 202 | return false; 203 | } 204 | 205 | const date = new Date( 206 | dateInfo.year || today.getFullYear(), 207 | dateInfo.month || 0, 208 | dateInfo.day || 1, 209 | dateInfo.hour || 0, 210 | dateInfo.minute || 0, 211 | dateInfo.second || 0 212 | ); 213 | return date; 214 | }; 215 | 216 | export default { 217 | format, 218 | parse 219 | }; 220 | -------------------------------------------------------------------------------- /docs/pages/track-view.vue: -------------------------------------------------------------------------------- 1 | 136 | 137 | 235 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 宋慧武 3 | * @Date: 2019-03-06 17:49:29 4 | * @Last Modified by: 宋慧武 5 | * @Last Modified time: 2020-06-05 20:23:06 6 | */ 7 | import { 8 | zipArray, 9 | exactlySameVnode, 10 | _exactMatch, 11 | _partialMatch 12 | } from "../utils/helper"; 13 | import * as debug from "../utils/debug"; 14 | import { isVisible } from "../utils/dom"; 15 | import VisMonitor from "../utils/vis-monitor"; 16 | 17 | const MODIFIERS = ["async", "delay", "watch", "show", "once", "custom"]; // 修饰符 18 | 19 | /******************************************************************************* 20 | * @desc 监听数据发生改变时触发埋点,需处理两种情况: 21 | * ① 初始化时开始监听 v-track:xxxxx.watch="{ common_exp }" 22 | * ops.immediate 表示初始化时立即开始监听 23 | * 24 | * ② 点击事件之后开始监听 v-track:18016.click.async="{ refreshHotSpot, exposureId }" 25 | * el.contains(this.target) 避免多个“地方”同时监听同一个值出现多次上报的问题 26 | *******************************************************************************/ 27 | function _watcher(el, exp, cbk, ctt, ops = {}) { 28 | el.$unwatch = ctt.$watch( 29 | () => ctt[exp], 30 | (nv, ov) => { 31 | nv !== ov && (ops.immediate || el.contains(this.target)) && cbk(); 32 | this.target = null; // 释放当前操作的watcher 33 | } 34 | ); 35 | } 36 | 37 | /************************************************************************* 38 | * @desc 自定义指令 v-track 39 | * 40 | * @param {*} el 指令所绑定的元素 41 | * @param {String} arg 埋点对应event ID 42 | * @param {Boolean} modifiers.click true: 事件行为埋点; false: 页面级埋点 43 | * @param {Boolean} modifiers.watch 异步埋点 44 | * @param {Boolean} modifiers.async 点击事件异步埋点 45 | * @param {Boolean} modifiers.delay 埋点是否延迟执行,默认先执行埋点再执行cbk 46 | * 47 | * @property {Function} tck 对应埋点方法 48 | * 49 | * @example v-track:18015 50 | * @example v-track:18015.watch 51 | * @example v-track:18015.watch.delay 52 | * @example v-track:18015.click 53 | * @example v-track:18015.click.async 54 | * @example v-track:18015.click.delay 55 | * @example v-track:18015.[自定义事件名].delay 56 | * @example v-track:18015.[自定义事件名].async 57 | *************************************************************************/ 58 | export function bind( 59 | el, 60 | { value, arg: id, modifiers, rawName }, 61 | { context, componentInstance }, 62 | _, 63 | __, 64 | events 65 | ) { 66 | if (!events[id]) throw new Error("tracking event does not exist"); 67 | 68 | let queue = []; 69 | let tck = events[id].bind(null, context); 70 | const watcher = (exp, cbk, ops) => 71 | _watcher.call(this, el, exp, cbk, context, ops); 72 | const exactMatch = (...args) => _exactMatch.call(null, modifiers, args); 73 | const partialMatch = (...args) => _partialMatch.call(null, modifiers, args); 74 | 75 | if (!Object.keys(modifiers).length) { 76 | events[id](context, value); 77 | } 78 | // 异步埋点 79 | else if (exactMatch("watch")) { 80 | const exp = Object.keys(value).shift(); 81 | 82 | watcher(exp, tck, { 83 | immediate: true 84 | }); 85 | } 86 | // 指定延长时间埋点 87 | else if (exactMatch("delay")) { 88 | el.$timer && clearTimeout(el.$timer); 89 | el.$timer = setTimeout(() => { 90 | events[id](context); 91 | }, value); 92 | } else if (exactMatch("watch", "delay")) { 93 | const { delay, ...args } = value; 94 | const exp = [...Object.keys(args)].pop(); 95 | 96 | tck = () => { 97 | el.$timer && clearTimeout(el.$timer); 98 | el.$timer = setTimeout(() => { 99 | const visible = isVisible(context.$el); 100 | 101 | visible && events[id](context); 102 | }, delay); 103 | }; 104 | watcher(exp, tck, { 105 | immediate: true 106 | }); 107 | } 108 | // 区域曝光埋点 109 | else if (partialMatch("show")) { 110 | const [args] = zipArray(value); 111 | const tck = events[id].bind(null, context, ...args); 112 | const once = partialMatch("once"); 113 | const custom = partialMatch("custom"); 114 | 115 | if (!el.$visMonitor) { 116 | setTimeout(() => { 117 | const vm = new VisMonitor( 118 | el, 119 | custom && context.$refs[value.ref], 120 | value && context.$refs[value.viewport], 121 | value && value.percent 122 | ); 123 | 124 | (once ? vm.$once : vm.$on).call(vm, "fullyvisible", tck); 125 | el.$visMonitor = vm; 126 | }, 0); 127 | } 128 | } else if ( 129 | (!componentInstance && modifiers.click) || 130 | (componentInstance && partialMatch("native")) 131 | ) { 132 | /** 133 | * @desc DOM元素事件行为埋点(需区分是否带参数) 134 | * @var {Function} fn 获取第一个参数作为回调函数 135 | * @var {String} exp 获取最后一个参数并作为监听对象 136 | */ 137 | switch (typeof value) { 138 | case "object": { 139 | const [args, keys] = zipArray(value); 140 | const fn = args.shift(); 141 | const exp = [...keys].pop(); 142 | 143 | debug.checkFun(fn); 144 | tck = events[id].bind(null, context, ...args); 145 | queue = [tck, fn.bind(null, ...args)]; 146 | modifiers.delay && queue.reverse(); 147 | modifiers.async && watcher(exp, queue.shift()); 148 | break; 149 | } 150 | case "function": 151 | queue = [tck, value]; 152 | modifiers.delay && queue.reverse(); 153 | break; 154 | } 155 | el.$listener = e => { 156 | this.target = e.target; 157 | queue.forEach(sub => sub(e)); 158 | }; 159 | el.addEventListener("click", el.$listener); 160 | } else if ( 161 | /** 162 | * @desc 组件自定义事件行为埋点(需区分是否带参数) 163 | * @var {Function} fn 获取第一个参数作为回调函数 164 | * @var {String} exp 获取最后一个参数并作为监听对象 165 | */ 166 | componentInstance && 167 | componentInstance.$el === el 168 | ) { 169 | let args, keys, fn, exp; 170 | const eventName = Object.keys(modifiers) 171 | .filter(key => !MODIFIERS.includes(key)) 172 | .pop(); 173 | 174 | if (typeof value === "object") { 175 | [args, keys] = zipArray(value); 176 | fn = args.shift(); 177 | exp = [...keys].pop(); 178 | debug.checkFun(fn); 179 | } 180 | 181 | if (el[`$on_${eventName}`]) return; 182 | componentInstance.$on(eventName, (...data) => { 183 | this.target = el; 184 | tck = events[id].bind(null, context, ...data); 185 | queue = [tck, (fn || value).bind(null, ...data)]; 186 | modifiers.delay && queue.reverse(); 187 | modifiers.async && watcher(exp, queue.shift()); 188 | queue.forEach(sub => sub()); 189 | el[`$on_${eventName}`] = true; // 避免重复监听 190 | }); 191 | } else { 192 | throw new Error(`${rawName} directive is not supported`); 193 | } 194 | } 195 | 196 | /** 197 | * @desc 由于 DOM 更新采用 diff 算法更新,如果新旧节点相同,则 el 会全等,导致 bind 绑定无法更 198 | * 新,出现事件绑定诡异的问题,但由于 DOM update 执行频率很高,会导致性能问题,所以这里加 199 | * 了一层exactlySameVnode过滤,即只有在新旧节点发生变化时才会重新绑定,否则相反 200 | * 201 | * @param {*} el 同bind 202 | * @param {...any} args 同bind 203 | */ 204 | export function updated(el, ...args) { 205 | if (!el.$listener) return; 206 | if (!exactlySameVnode(args[1], args[2])) { 207 | unbind.call(this, el); 208 | bind.call(this, el, ...args); 209 | } 210 | } 211 | 212 | export function unbind(el) { 213 | el.$listener && el.removeEventListener("click", el.$listener); 214 | el.$timer && clearTimeout(el.$timer); 215 | el.$unwatch && el.$unwatch(); 216 | el.$visMonitor && el.$visMonitor.destroy(); 217 | } 218 | -------------------------------------------------------------------------------- /docs/pages/home.vue: -------------------------------------------------------------------------------- 1 | 149 | 150 | 270 | -------------------------------------------------------------------------------- /docs/assets/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /docs/pages/custom-events.vue: -------------------------------------------------------------------------------- 1 | 164 | 165 | 292 | -------------------------------------------------------------------------------- /docs/dist/css/chunk-vendors.c687a9b2.css: -------------------------------------------------------------------------------- 1 | .el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #ebeef5;position:fixed;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{margin-left:13px;margin-right:8px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67c23a}.el-notification .el-icon-error{color:#f56c6c}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#e6a23c}.el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.el-notification-fade-leave-active{opacity:0}@font-face{font-family:element-icons;src:url(../fonts/element-icons.535877f5.woff) format("woff"),url(../fonts/element-icons.732389de.ttf) format("truetype");font-weight:400;font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\E6A0"}.el-icon-ice-cream-square:before{content:"\E6A3"}.el-icon-lollipop:before{content:"\E6A4"}.el-icon-potato-strips:before{content:"\E6A5"}.el-icon-milk-tea:before{content:"\E6A6"}.el-icon-ice-drink:before{content:"\E6A7"}.el-icon-ice-tea:before{content:"\E6A9"}.el-icon-coffee:before{content:"\E6AA"}.el-icon-orange:before{content:"\E6AB"}.el-icon-pear:before{content:"\E6AC"}.el-icon-apple:before{content:"\E6AD"}.el-icon-cherry:before{content:"\E6AE"}.el-icon-watermelon:before{content:"\E6AF"}.el-icon-grape:before{content:"\E6B0"}.el-icon-refrigerator:before{content:"\E6B1"}.el-icon-goblet-square-full:before{content:"\E6B2"}.el-icon-goblet-square:before{content:"\E6B3"}.el-icon-goblet-full:before{content:"\E6B4"}.el-icon-goblet:before{content:"\E6B5"}.el-icon-cold-drink:before{content:"\E6B6"}.el-icon-coffee-cup:before{content:"\E6B8"}.el-icon-water-cup:before{content:"\E6B9"}.el-icon-hot-water:before{content:"\E6BA"}.el-icon-ice-cream:before{content:"\E6BB"}.el-icon-dessert:before{content:"\E6BC"}.el-icon-sugar:before{content:"\E6BD"}.el-icon-tableware:before{content:"\E6BE"}.el-icon-burger:before{content:"\E6BF"}.el-icon-knife-fork:before{content:"\E6C1"}.el-icon-fork-spoon:before{content:"\E6C2"}.el-icon-chicken:before{content:"\E6C3"}.el-icon-food:before{content:"\E6C4"}.el-icon-dish-1:before{content:"\E6C5"}.el-icon-dish:before{content:"\E6C6"}.el-icon-moon-night:before{content:"\E6EE"}.el-icon-moon:before{content:"\E6F0"}.el-icon-cloudy-and-sunny:before{content:"\E6F1"}.el-icon-partly-cloudy:before{content:"\E6F2"}.el-icon-cloudy:before{content:"\E6F3"}.el-icon-sunny:before{content:"\E6F6"}.el-icon-sunset:before{content:"\E6F7"}.el-icon-sunrise-1:before{content:"\E6F8"}.el-icon-sunrise:before{content:"\E6F9"}.el-icon-heavy-rain:before{content:"\E6FA"}.el-icon-lightning:before{content:"\E6FB"}.el-icon-light-rain:before{content:"\E6FC"}.el-icon-wind-power:before{content:"\E6FD"}.el-icon-baseball:before{content:"\E712"}.el-icon-soccer:before{content:"\E713"}.el-icon-football:before{content:"\E715"}.el-icon-basketball:before{content:"\E716"}.el-icon-ship:before{content:"\E73F"}.el-icon-truck:before{content:"\E740"}.el-icon-bicycle:before{content:"\E741"}.el-icon-mobile-phone:before{content:"\E6D3"}.el-icon-service:before{content:"\E6D4"}.el-icon-key:before{content:"\E6E2"}.el-icon-unlock:before{content:"\E6E4"}.el-icon-lock:before{content:"\E6E5"}.el-icon-watch:before{content:"\E6FE"}.el-icon-watch-1:before{content:"\E6FF"}.el-icon-timer:before{content:"\E702"}.el-icon-alarm-clock:before{content:"\E703"}.el-icon-map-location:before{content:"\E704"}.el-icon-delete-location:before{content:"\E705"}.el-icon-add-location:before{content:"\E706"}.el-icon-location-information:before{content:"\E707"}.el-icon-location-outline:before{content:"\E708"}.el-icon-location:before{content:"\E79E"}.el-icon-place:before{content:"\E709"}.el-icon-discover:before{content:"\E70A"}.el-icon-first-aid-kit:before{content:"\E70B"}.el-icon-trophy-1:before{content:"\E70C"}.el-icon-trophy:before{content:"\E70D"}.el-icon-medal:before{content:"\E70E"}.el-icon-medal-1:before{content:"\E70F"}.el-icon-stopwatch:before{content:"\E710"}.el-icon-mic:before{content:"\E711"}.el-icon-copy-document:before{content:"\E718"}.el-icon-full-screen:before{content:"\E719"}.el-icon-switch-button:before{content:"\E71B"}.el-icon-aim:before{content:"\E71C"}.el-icon-crop:before{content:"\E71D"}.el-icon-odometer:before{content:"\E71E"}.el-icon-time:before{content:"\E71F"}.el-icon-bangzhu:before{content:"\E724"}.el-icon-close-notification:before{content:"\E726"}.el-icon-microphone:before{content:"\E727"}.el-icon-turn-off-microphone:before{content:"\E728"}.el-icon-position:before{content:"\E729"}.el-icon-postcard:before{content:"\E72A"}.el-icon-message:before{content:"\E72B"}.el-icon-chat-line-square:before{content:"\E72D"}.el-icon-chat-dot-square:before{content:"\E72E"}.el-icon-chat-dot-round:before{content:"\E72F"}.el-icon-chat-square:before{content:"\E730"}.el-icon-chat-line-round:before{content:"\E731"}.el-icon-chat-round:before{content:"\E732"}.el-icon-set-up:before{content:"\E733"}.el-icon-turn-off:before{content:"\E734"}.el-icon-open:before{content:"\E735"}.el-icon-connection:before{content:"\E736"}.el-icon-link:before{content:"\E737"}.el-icon-cpu:before{content:"\E738"}.el-icon-thumb:before{content:"\E739"}.el-icon-female:before{content:"\E73A"}.el-icon-male:before{content:"\E73B"}.el-icon-guide:before{content:"\E73C"}.el-icon-news:before{content:"\E73E"}.el-icon-price-tag:before{content:"\E744"}.el-icon-discount:before{content:"\E745"}.el-icon-wallet:before{content:"\E747"}.el-icon-coin:before{content:"\E748"}.el-icon-money:before{content:"\E749"}.el-icon-bank-card:before{content:"\E74A"}.el-icon-box:before{content:"\E74B"}.el-icon-present:before{content:"\E74C"}.el-icon-sell:before{content:"\E6D5"}.el-icon-sold-out:before{content:"\E6D6"}.el-icon-shopping-bag-2:before{content:"\E74D"}.el-icon-shopping-bag-1:before{content:"\E74E"}.el-icon-shopping-cart-2:before{content:"\E74F"}.el-icon-shopping-cart-1:before{content:"\E750"}.el-icon-shopping-cart-full:before{content:"\E751"}.el-icon-smoking:before{content:"\E752"}.el-icon-no-smoking:before{content:"\E753"}.el-icon-house:before{content:"\E754"}.el-icon-table-lamp:before{content:"\E755"}.el-icon-school:before{content:"\E756"}.el-icon-office-building:before{content:"\E757"}.el-icon-toilet-paper:before{content:"\E758"}.el-icon-notebook-2:before{content:"\E759"}.el-icon-notebook-1:before{content:"\E75A"}.el-icon-files:before{content:"\E75B"}.el-icon-collection:before{content:"\E75C"}.el-icon-receiving:before{content:"\E75D"}.el-icon-suitcase-1:before{content:"\E760"}.el-icon-suitcase:before{content:"\E761"}.el-icon-film:before{content:"\E763"}.el-icon-collection-tag:before{content:"\E765"}.el-icon-data-analysis:before{content:"\E766"}.el-icon-pie-chart:before{content:"\E767"}.el-icon-data-board:before{content:"\E768"}.el-icon-data-line:before{content:"\E76D"}.el-icon-reading:before{content:"\E769"}.el-icon-magic-stick:before{content:"\E76A"}.el-icon-coordinate:before{content:"\E76B"}.el-icon-mouse:before{content:"\E76C"}.el-icon-brush:before{content:"\E76E"}.el-icon-headset:before{content:"\E76F"}.el-icon-umbrella:before{content:"\E770"}.el-icon-scissors:before{content:"\E771"}.el-icon-mobile:before{content:"\E773"}.el-icon-attract:before{content:"\E774"}.el-icon-monitor:before{content:"\E775"}.el-icon-search:before{content:"\E778"}.el-icon-takeaway-box:before{content:"\E77A"}.el-icon-paperclip:before{content:"\E77D"}.el-icon-printer:before{content:"\E77E"}.el-icon-document-add:before{content:"\E782"}.el-icon-document:before{content:"\E785"}.el-icon-document-checked:before{content:"\E786"}.el-icon-document-copy:before{content:"\E787"}.el-icon-document-delete:before{content:"\E788"}.el-icon-document-remove:before{content:"\E789"}.el-icon-tickets:before{content:"\E78B"}.el-icon-folder-checked:before{content:"\E77F"}.el-icon-folder-delete:before{content:"\E780"}.el-icon-folder-remove:before{content:"\E781"}.el-icon-folder-add:before{content:"\E783"}.el-icon-folder-opened:before{content:"\E784"}.el-icon-folder:before{content:"\E78A"}.el-icon-edit-outline:before{content:"\E764"}.el-icon-edit:before{content:"\E78C"}.el-icon-date:before{content:"\E78E"}.el-icon-c-scale-to-original:before{content:"\E7C6"}.el-icon-view:before{content:"\E6CE"}.el-icon-loading:before{content:"\E6CF"}.el-icon-rank:before{content:"\E6D1"}.el-icon-sort-down:before{content:"\E7C4"}.el-icon-sort-up:before{content:"\E7C5"}.el-icon-sort:before{content:"\E6D2"}.el-icon-finished:before{content:"\E6CD"}.el-icon-refresh-left:before{content:"\E6C7"}.el-icon-refresh-right:before{content:"\E6C8"}.el-icon-refresh:before{content:"\E6D0"}.el-icon-video-play:before{content:"\E7C0"}.el-icon-video-pause:before{content:"\E7C1"}.el-icon-d-arrow-right:before{content:"\E6DC"}.el-icon-d-arrow-left:before{content:"\E6DD"}.el-icon-arrow-up:before{content:"\E6E1"}.el-icon-arrow-down:before{content:"\E6DF"}.el-icon-arrow-right:before{content:"\E6E0"}.el-icon-arrow-left:before{content:"\E6DE"}.el-icon-top-right:before{content:"\E6E7"}.el-icon-top-left:before{content:"\E6E8"}.el-icon-top:before{content:"\E6E6"}.el-icon-bottom:before{content:"\E6EB"}.el-icon-right:before{content:"\E6E9"}.el-icon-back:before{content:"\E6EA"}.el-icon-bottom-right:before{content:"\E6EC"}.el-icon-bottom-left:before{content:"\E6ED"}.el-icon-caret-top:before{content:"\E78F"}.el-icon-caret-bottom:before{content:"\E790"}.el-icon-caret-right:before{content:"\E791"}.el-icon-caret-left:before{content:"\E792"}.el-icon-d-caret:before{content:"\E79A"}.el-icon-share:before{content:"\E793"}.el-icon-menu:before{content:"\E798"}.el-icon-s-grid:before{content:"\E7A6"}.el-icon-s-check:before{content:"\E7A7"}.el-icon-s-data:before{content:"\E7A8"}.el-icon-s-opportunity:before{content:"\E7AA"}.el-icon-s-custom:before{content:"\E7AB"}.el-icon-s-claim:before{content:"\E7AD"}.el-icon-s-finance:before{content:"\E7AE"}.el-icon-s-comment:before{content:"\E7AF"}.el-icon-s-flag:before{content:"\E7B0"}.el-icon-s-marketing:before{content:"\E7B1"}.el-icon-s-shop:before{content:"\E7B4"}.el-icon-s-open:before{content:"\E7B5"}.el-icon-s-management:before{content:"\E7B6"}.el-icon-s-ticket:before{content:"\E7B7"}.el-icon-s-release:before{content:"\E7B8"}.el-icon-s-home:before{content:"\E7B9"}.el-icon-s-promotion:before{content:"\E7BA"}.el-icon-s-operation:before{content:"\E7BB"}.el-icon-s-unfold:before{content:"\E7BC"}.el-icon-s-fold:before{content:"\E7A9"}.el-icon-s-platform:before{content:"\E7BD"}.el-icon-s-order:before{content:"\E7BE"}.el-icon-s-cooperation:before{content:"\E7BF"}.el-icon-bell:before{content:"\E725"}.el-icon-message-solid:before{content:"\E799"}.el-icon-video-camera:before{content:"\E772"}.el-icon-video-camera-solid:before{content:"\E796"}.el-icon-camera:before{content:"\E779"}.el-icon-camera-solid:before{content:"\E79B"}.el-icon-download:before{content:"\E77C"}.el-icon-upload2:before{content:"\E77B"}.el-icon-upload:before{content:"\E7C3"}.el-icon-picture-outline-round:before{content:"\E75F"}.el-icon-picture-outline:before{content:"\E75E"}.el-icon-picture:before{content:"\E79F"}.el-icon-close:before{content:"\E6DB"}.el-icon-check:before{content:"\E6DA"}.el-icon-plus:before{content:"\E6D9"}.el-icon-minus:before{content:"\E6D8"}.el-icon-help:before{content:"\E73D"}.el-icon-s-help:before{content:"\E7B3"}.el-icon-circle-close:before{content:"\E78D"}.el-icon-circle-check:before{content:"\E720"}.el-icon-circle-plus-outline:before{content:"\E723"}.el-icon-remove-outline:before{content:"\E722"}.el-icon-zoom-out:before{content:"\E776"}.el-icon-zoom-in:before{content:"\E777"}.el-icon-error:before{content:"\E79D"}.el-icon-success:before{content:"\E79C"}.el-icon-circle-plus:before{content:"\E7A0"}.el-icon-remove:before{content:"\E7A2"}.el-icon-info:before{content:"\E7A1"}.el-icon-question:before{content:"\E7A4"}.el-icon-warning-outline:before{content:"\E6C9"}.el-icon-warning:before{content:"\E7A3"}.el-icon-goods:before{content:"\E7C2"}.el-icon-s-goods:before{content:"\E7B2"}.el-icon-star-off:before{content:"\E717"}.el-icon-star-on:before{content:"\E797"}.el-icon-more-outline:before{content:"\E6CC"}.el-icon-more:before{content:"\E794"}.el-icon-phone-outline:before{content:"\E6CB"}.el-icon-phone:before{content:"\E795"}.el-icon-user:before{content:"\E6E3"}.el-icon-user-solid:before{content:"\E7A5"}.el-icon-setting:before{content:"\E6CA"}.el-icon-s-tools:before{content:"\E7AC"}.el-icon-delete:before{content:"\E6D7"}.el-icon-delete-solid:before{content:"\E7C9"}.el-icon-eleme:before{content:"\E7C7"}.el-icon-platform-eleme:before{content:"\E7CA"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotating{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.el-message__closeBtn:focus,.el-message__content:focus{outline-width:0}.el-message{min-width:380px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#ebeef5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,transform .4s,top .4s;transition:opacity .3s,transform .4s,top .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67c23a}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#e6a23c}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#f56c6c}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#c0c4cc;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67c23a}.el-message .el-icon-error{color:#f56c6c}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#e6a23c}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}.el-fade-in-enter,.el-fade-in-leave-active,.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active,.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.el-fade-in-enter-active,.el-fade-in-leave-active,.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1);transform:scale(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45);transform:scale(.45)}.collapse-transition{-webkit-transition:height .3s ease-in-out,padding-top .3s ease-in-out,padding-bottom .3s ease-in-out;transition:height .3s ease-in-out,padding-top .3s ease-in-out,padding-bottom .3s ease-in-out}.horizontal-collapse-transition{-webkit-transition:width .3s ease-in-out,padding-left .3s ease-in-out,padding-right .3s ease-in-out;transition:width .3s ease-in-out,padding-left .3s ease-in-out,padding-right .3s ease-in-out}.el-list-enter-active,.el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-collapse{border-top:1px solid #ebeef5;border-bottom:1px solid #ebeef5}.el-collapse-item.is-disabled .el-collapse-item__header{color:#bbb;cursor:not-allowed}.el-collapse-item__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:48px;line-height:48px;background-color:#fff;color:#303133;cursor:pointer;border-bottom:1px solid #ebeef5;font-size:13px;font-weight:500;-webkit-transition:border-bottom-color .3s;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin:0 8px 0 auto;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-weight:300}.el-collapse-item__arrow.is-active{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-collapse-item__header.focusing:focus:not(:hover){color:#409eff}.el-collapse-item__header.is-active{border-bottom-color:transparent}.el-collapse-item__wrap{will-change:height;background-color:#fff;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;border-bottom:1px solid #ebeef5}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item:last-child{margin-bottom:-1px}.el-card{border-radius:4px;border:1px solid #ebeef5;background-color:#fff;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__body{padding:20px}.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-light .el-alert__closebtn{color:#c0c4cc}.el-alert.is-dark .el-alert__closebtn,.el-alert.is-dark .el-alert__description{color:#fff}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success.is-light{background-color:#f0f9eb;color:#67c23a}.el-alert--success.is-light .el-alert__description{color:#67c23a}.el-alert--success.is-dark{background-color:#67c23a;color:#fff}.el-alert--info.is-light{background-color:#f4f4f5;color:#909399}.el-alert--info.is-dark{background-color:#909399;color:#fff}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning.is-light{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning.is-light .el-alert__description{color:#e6a23c}.el-alert--warning.is-dark{background-color:#e6a23c;color:#fff}.el-alert--error.is-light{background-color:#fef0f0;color:#f56c6c}.el-alert--error.is-light .el-alert__description{color:#f56c6c}.el-alert--error.is-dark{background-color:#f56c6c;color:#fff}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-alert-fade-enter,.el-alert-fade-leave-active{opacity:0} -------------------------------------------------------------------------------- /docs/dist/js/app.efe84ade.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var a,i,c=e[0],o=e[1],l=e[2],p=0,v=[];pclick me
\n',z='\n\n',Y='\n
click me
\n',J="\nexport default {\n data: () => ({\n item: {\n id: Date.now().toString(36)\n },\n })\n}\n",Q='\n
click me
\n',X="\nexport default {\n data() {\n return {\n index: 0\n };\n },\n methods: {\n handleClickDelay() {\n this.index++;\n },\n }\n}\n",Z='\n
click me
\n',tt='\nexport default {\n data() {\n return {\n rest: null\n };\n },\n methods: {\n async handleClickAsync() {\n const response = await new Promise(resolve => {\n setTimeout(() => {\n resolve({ data: "success" });\n }, 300);\n });\n\n this.rest = response.data;\n }\n }\n}\n',et={name:"Home",components:{Button:q,CodeSnippet:B},data:function(){return{trackClickSnippet:G,trackNativeClickSnippet:z,trackClickWithParamSnippet:Y,jsTrackClickWithParamSnippet:J,trackClickDelaySnippet:Q,trackClickAsyncSnippet:Z,jsTrackClickDelaySnippet:X,jsTrackClickAsyncSnippet:tt,item:{id:P()().toString(36)},index:0,rest:null}},methods:{handleClick:function(){this.$message.success("事件执行成功")},handleNativeEvent:function(){this.$message.success("组件原生事件执行成功")},handleClickWithParam:function(t,e){var n=e.target;this.$message.success("事件执行成功,参数为".concat(x()(t),"--").concat(n))},handleClickDelay:function(){this.index++},handleClickAsync:function(){var t=Object($["a"])(regeneratorRuntime.mark(function t(){var e;return regeneratorRuntime.wrap(function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,new T.a(function(t){setTimeout(function(){t({data:"success"})},300)});case 2:e=t.sent,this.rest===e.data?this.$message.success("异步事件返回成功,但返回结果和上一次相等,埋点不会上报"):(this.rest=e.data,this.$message.success("异步事件返回成功"));case 4:case"end":return t.stop()}},t,this)}));function e(){return t.apply(this,arguments)}return e}()}},nt=et,at=Object(w["a"])(nt,S,E,!1,null,null,null),rt=at.exports,st=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"page-started page"},[n("section",{staticClass:"nav"},[n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 返回\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 事件行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"CUSTOM_EVENTS"}}},[t._v("\n 自定义事件埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"TRACK_VIEW"}}},[t._v("\n 页面行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"BLOCK_SHOW"}}},[t._v("\n 区域展现埋点\n ")]),t._v(" "),n("a",{attrs:{href:"https://github.com/l-hammer/v-track/issues"}},[t._v("打开一个 issue")])],1),t._v(" "),t._m(0),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{code:t.installSnippet,lang:"shell"}}),t._v(" "),n("div",{staticClass:"plus"},[t._v("+")]),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{code:t.mainSnippet,lang:"js"}}),t._v(" "),n("div",{staticClass:"plus"},[t._v("+")]),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{code:t.eventsSnippet,lang:"js"}}),t._v(" "),n("div",{staticClass:"plus"},[t._v("+")]),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{code:t.componentSnippet,lang:"html"}})],1)])},it=[function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("section",{staticClass:"snippets"},[n("div",{staticClass:"section-content small"},[t._v("\n v-track通过\n "),n("a",{attrs:{href:"https://cn.vuejs.org/v2/guide/custom-directive.html"}},[t._v("Vue 自定义指令")]),t._v("的方式将埋点代码与业务代码完全解耦,完整示例可参考\n "),n("a",{attrs:{href:"https://github.com/l-hammer/v-track/tree/master"}},[t._v("GitHub")])])])}],ct="\n# YARN\n$ yarn add v-track\n\n# NPM\n$ npm install v-track --save\n",ot='\nimport Vue from "vue";\nimport VTrack from "v-track";\nimport trackEvents from "./track-events";\n\nVue.use(VTrack, {\n trackEvents, // 埋点事件对象\n trackEnable: {\n UVPV: true, // 是否开启UVPV统计,v0.8.3新增routeUpdate,即在当前路由参数发生改变时埋点,默认为false\n TONP: true // 是否开启页面停留时长统计,默认为false\n }\n})\n',lt='\nimport trackAction from "./action"; // 自定义埋点上报的方法\n\nexport default {\n /**\n * @name UVPV 固定名称不支持修改\n * @desc UV、PV埋点\n * @param {Object} context 当前上下文\n */\n UVPV(_) {\n trackAction("1");\n },\n /**\n * @name TONP 固定名称不支持修改\n * @desc 页面停留时间埋点(Time on Page)\n * @param {Object} context 当前上下文\n * @param {Timestamp} et 进入页面时间\n * @param {Timestamp} dt 离开页面时间\n */\n TONP(_, { et, dt }) {\n trackAction("2", {\n stt: dt - et\n });\n },\n /**\n * @param {Object} context 当前上下文\n * @param {Object} item 事件参数\n * @param {Object} event 事件对象\n */\n 18015(\n {\n $route: { name }\n },\n { id },\n { target }\n ) {\n trackAction("18015", {\n id,\n source_page: name,\n target\n });\n },\n};\n',ut='\n\x3c!-- 页面行为埋点(track-view为v-track全局注册的组件) --\x3e\n\n\n\n\n\n\x3c!-- 事件行为埋点(DOM) --\x3e\n
\n
\n
\n
\n\n\x3c!-- 事件行为埋点(组件) --\x3e\n\n\n\n\n\n\x3c!-- 区域展现埋点(block 可以是 DOM 或者组件) --\x3e\n\n\n\n\n',pt={name:"Started",components:{CodeSnippet:B},data:function(){return{installSnippet:ct,mainSnippet:ot,eventsSnippet:lt,componentSnippet:ut}}},vt=pt,dt=Object(w["a"])(vt,st,it,!1,null,null,null),mt=dt.exports,ht=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"page-home page"},[n("section",{staticClass:"nav"},[n("router-link",{attrs:{to:{name:"STARTED"}}},[t._v("\n 快速开始\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 事件行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 返回\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"TRACK_VIEW"}}},[t._v("\n 页面行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"BLOCK_SHOW"}}},[t._v("\n 区域展现埋点\n ")]),t._v(" "),n("a",{attrs:{href:"https://github.com/l-hammer/v-track/issues"}},[t._v("打开一个 issue")])],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个自定义事件行为埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content"},[n("Button",{directives:[{name:"track",rawName:"v-track:18015.custom-event",value:t.handleCustomEvent,expression:"handleCustomEvent",arg:"18015",modifiers:{"custom-event":!0}}]})],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.ButtonComponentSnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackCustomEventSnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个有带参数的自定义事件埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content"},[n("Button",{directives:[{name:"track",rawName:"v-track:18019.custom-event",value:t.handleCustomEventWithParam,expression:"handleCustomEventWithParam",arg:"18019",modifiers:{"custom-event":!0}}]})],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.ButtonComponentWithParamSnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackCustomEventWithParamSnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个发生在自定义事件之后的埋点,默认先执行埋点再执行自定义事件。如示例所示:activeName初始值为Feedback,自定义事件会更新该值,所以埋点获取到activeName的值应为当前激活面板的name",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content collapse-wrapper"},[n("el-collapse",{directives:[{name:"track",rawName:"v-track:18020.change.delay",value:t.handleChange,expression:"handleChange",arg:"18020",modifiers:{change:!0,delay:!0}}],attrs:{accordion:""},model:{value:t.activeName,callback:function(e){t.activeName=e},expression:"activeName"}},[n("el-collapse-item",{attrs:{title:"反馈 Feedback",name:"Feedback"}},[n("div",[t._v("\n 控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;\n ")]),t._v(" "),n("div",[t._v("页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。")])]),t._v(" "),n("el-collapse-item",{attrs:{title:"效率 Efficiency",name:"Efficiency"}},[n("div",[t._v("简化流程:设计简洁直观的操作流程;")]),t._v(" "),n("div",[t._v("\n 清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;\n ")]),t._v(" "),n("div",[t._v("\n 帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。\n ")])]),t._v(" "),n("el-collapse-item",{attrs:{title:"可控 Controllability",name:"Controllability"}},[n("div",[t._v("\n 用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;\n ")]),t._v(" "),n("div",[t._v("\n 结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。\n ")])])],1)],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackCustomEventDelaySnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"js",code:t.jsTrackCustomEventDelaySnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个有异步行为的自定义事件埋点。如示例所示:rest初始值为null,自定义事件会fetch为success,所以埋点获取到的rest值应该为success",closable:!1}}),n("el-alert",{attrs:{center:"",type:"warning",title:"备注:同普通DOM事件埋点一样,只有在 rest 发生变化时才会触发埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content"},[n("Button",{directives:[{name:"track",rawName:"v-track:18021.custom-event.async",value:{fetchRest:t.fetchRest,rest:t.rest},expression:"{ fetchRest, rest }",arg:"18021",modifiers:{"custom-event":!0,async:!0}}]})],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackCustomEventAsyncSnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"js",code:t.jsTrackCustomEventAsyncSnippet}})],1)])},ft=[],kt='\n\x3c!-- button 组件 --\x3e\n
\n click me\n
\n',gt='\n\x3c!-- button 组件 --\x3e\n
\n click me\n
\n',yt='\n\n',Ct='\n\n',wt='\n\n ...\n ...\n ...\n\n',_t='\nexport default {\n data() {\n return {\n activeName: "Feedback",\n };\n },\n methods: {\n handleChange(val) {\n this.activeName = val;\n }\n }\n}\n',bt='\n\n',St='\nexport default {\n data() {\n return {\n rest: null\n };\n },\n methods: {\n async fetchRest() {\n const response = await new Promise(resolve => {\n setTimeout(() => {\n resolve({ data: "success" });\n }, 300);\n });\n\n this.rest = response.data;\n }\n }\n}\n',Et={name:"CustomEvents",components:{Button:q,CodeSnippet:B},data:function(){return{ButtonComponentSnippet:kt,ButtonComponentWithParamSnippet:gt,trackCustomEventSnippet:yt,trackCustomEventWithParamSnippet:Ct,trackCustomEventDelaySnippet:wt,trackCustomEventAsyncSnippet:bt,jsTrackCustomEventDelaySnippet:_t,jsTrackCustomEventAsyncSnippet:St,item:{id:Math.random().toString(36).substr(2)},activeName:"Feedback",rest:null}},methods:{handleCustomEvent:function(){this.$message.success("自定义事件执行成功")},handleCustomEventWithParam:function(t,e){var n=e.target;this.$message.success("自定义事件执行成功,参数为".concat(x()(t),"--").concat(n))},handleChange:function(t){this.activeName=t},fetchRest:function(){var t=Object($["a"])(regeneratorRuntime.mark(function t(){var e;return regeneratorRuntime.wrap(function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,new T.a(function(t){setTimeout(function(){t({data:"success"})},300)});case 2:e=t.sent,this.rest===e.data?this.$message.success("异步事件返回成功,但返回结果和上一次相等,埋点不会上报"):(this.rest=e.data,this.$message.success("异步事件返回成功"));case 4:case"end":return t.stop()}},t,this)}));function e(){return t.apply(this,arguments)}return e}()}},Ot=Et,Tt=Object(w["a"])(Ot,ht,ft,!1,null,null,null),$t=Tt.exports,jt=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"page-home page"},[n("section",{staticClass:"nav"},[n("router-link",{attrs:{to:{name:"STARTED"}}},[t._v("\n 快速开始\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 事件行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"CUSTOM_EVENTS"}}},[t._v("\n 自定义事件埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 返回\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"BLOCK_SHOW"}}},[t._v("\n 区域展现埋点\n ")]),t._v(" "),n("a",{attrs:{href:"https://github.com/l-hammer/v-track/issues"}},[t._v("打开一个 issue")])],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个页面初始化埋点",closable:!1}}),t._v(" "),n("el-alert",{attrs:{center:"",type:"warning",title:"备注:v-track指令可用于任何元素,此行为埋点建议绑定到页面的根元素或者v-track提供的track-view组件上",closable:!1}}),t._v(" "),n("div",{directives:[{name:"track",rawName:"v-track:18022",arg:"18022"}],staticClass:"section-content"})],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackViewComponentSnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"配合 v-if 指令控制埋点上报次数,即 show 为真时,会再次触发埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content"},[t.show?n("track-view",{directives:[{name:"track",rawName:"v-track:18023",arg:"18023"}]}):t._e(),t._v(" "),n("div",{staticClass:"track-button",on:{click:function(){return t.show=!t.show}}},[t._v("\n click me -> "+t._s(t.show)+"\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackViewComponentVIfSnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"js",code:t.jsTrackViewComponentVIfSnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个监听页面行为的埋点,同事件行为埋点一样,只有当返回结果发生变化时才会上报埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content"},[n("track-view",{directives:[{name:"track",rawName:"v-track:18025.watch",value:{rest1:t.rest1},expression:"{ rest1 }",arg:"18025",modifiers:{watch:!0}}]}),t._v(" "),n("div",{staticClass:"track-button",on:{click:function(e){return t.fetchRest(!1)}}},[t._v("\n click me\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackViewComponentWatchSnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"js",code:t.jsTrackViewComponentWatchSnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个监听页面行为并延时的埋点,不同于事件延时埋点,这里可通过delay设置具体的延长时间",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content"},[n("track-view",{directives:[{name:"track",rawName:"v-track:18026.watch.delay",value:{rest2:t.rest2,delay:2e3},expression:"{ rest2, delay: 2000 }",arg:"18026",modifiers:{watch:!0,delay:!0}}]}),t._v(" "),n("div",{staticClass:"track-button",on:{click:function(e){return t.fetchRest(!0)}}},[t._v("\n click me\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackViewComponentWatchDelaySnippet}}),t._v(" "),n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"js",code:t.jsTrackViewComponentWatchDelaySnippet}})],1)])},xt=[],Vt="\n\n",Pt='\n\n
click me -> {{ show }}
\n',Nt="\nexport default {\n data: () => ({ show: false })\n}\n",Mt='\n\n
click me
\n',At='\nexport default {\n data: () => ({ rest: null }),\n methods: {\n async fetchRest() {\n const response = await new Promise(resolve => {\n setTimeout(() => {\n resolve({ data: "success" });\n }, 300);\n });\n \n this.rest = response.data;\n }\n },\n}\n',Wt='\n\n
click me
\n',Ht='\nexport default {\n data: () => ({ rest: null }),\n methods: {\n async fetchRest() {\n const response = await new Promise(resolve => {\n setTimeout(() => {\n resolve({ data: "success" });\n }, 300);\n });\n \n this.rest = response.data;\n }\n },\n}\n',Rt={name:"TrackViews",components:{CodeSnippet:B},data:function(){return{trackViewComponentSnippet:Vt,trackViewComponentVIfSnippet:Pt,jsTrackViewComponentVIfSnippet:Nt,trackViewComponentWatchSnippet:Mt,jsTrackViewComponentWatchSnippet:At,trackViewComponentWatchDelaySnippet:Wt,jsTrackViewComponentWatchDelaySnippet:Ht,show:!1,rest1:null,rest2:null}},methods:{fetchRest:function(){var t=Object($["a"])(regeneratorRuntime.mark(function t(e){var n;return regeneratorRuntime.wrap(function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,new T.a(function(t){setTimeout(function(){t({data:"success"})},300)});case 2:n=t.sent,!e&&this.rest1===n.data||e&&this.rest2===n.data?this.$message.success("异步事件返回成功,但返回结果和上一次相等,埋点不会上报"):(this.$message.success("异步事件返回成功"),e?this.rest2=n.data:this.rest1=n.data);case 4:case"end":return t.stop()}},t,this)}));function e(e){return t.apply(this,arguments)}return e}()}},Dt=Rt,Bt=Object(w["a"])(Dt,jt,xt,!1,null,null,null),Ut=Bt.exports,It=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"page-home page"},[n("section",{staticClass:"nav"},[n("router-link",{attrs:{to:{name:"STARTED"}}},[t._v("\n 快速开始\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 事件行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"CUSTOM_EVENTS"}}},[t._v("\n 自定义事件埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"TRACK_VIEW"}}},[t._v("\n 页面行为埋点\n ")]),t._v(" "),n("router-link",{attrs:{to:{name:"HOME"}}},[t._v("\n 返回\n ")]),t._v(" "),n("a",{attrs:{href:"https://github.com/l-hammer/v-track/issues"}},[t._v("打开一个 issue")])],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个只会上报一次的区域展现埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content large"},[n("el-card",{directives:[{name:"track",rawName:"v-track:18028.show.once",arg:"18028",modifiers:{show:!0,once:!0}}],attrs:{shadow:"always"}},[t._v("\n 我只想被曝光一次\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackViewComponentOnceSnippet}})],1),t._v(" "),n("section",{staticClass:"demo"},[n("el-alert",{attrs:{center:"",type:"info",title:"这是一个会连续上报的区域展现埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content large"},[n("el-card",{directives:[{name:"track",rawName:"v-track:18027.show",arg:"18027",modifiers:{show:!0}}],attrs:{shadow:"always"}},[t._v("\n 我想被曝光无数次\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.trackViewComponentSnippet}})],1),t._v(" "),n("section",{ref:"viewport1",staticClass:"demo viewport"},[n("el-alert",{attrs:{center:"",type:"info",title:"某个区域内元素曝光埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content large"},[n("el-card",{directives:[{name:"track",rawName:"v-track:18029.show",value:{viewport:"viewport1"},expression:"{ viewport: 'viewport1' }",arg:"18029",modifiers:{show:!0}}],attrs:{shadow:"always"}},[t._v("\n 我也想被曝光无数次\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.viewportTrackViewComponentSnippet1}})],1),t._v(" "),n("section",{ref:"viewport2",staticClass:"demo viewport"},[n("el-alert",{attrs:{center:"",type:"info",title:"某个区域内元素曝光超过一半则上报埋点",closable:!1}}),t._v(" "),n("div",{staticClass:"section-content large"},[n("el-card",{directives:[{name:"track",rawName:"v-track:18030.show",value:{viewport:"viewport2",percent:.5},expression:"{ viewport: 'viewport2', percent: 0.5 }",arg:"18030",modifiers:{show:!0}}],attrs:{shadow:"always"}},[t._v("\n 我也想被曝光无数次\n ")])],1)],1),t._v(" "),n("section",{staticClass:"snippets"},[n("CodeSnippet",{staticClass:"snippet",attrs:{lang:"html",code:t.viewportTrackViewComponentSnippet2}})],1)])},Lt=[],Ft='\n我想被曝光无数次\n',Kt='\n我只想被曝光一次\n',qt='\n
\n 我也想被曝光无数次\n
\n',Gt='\n
\n 我也想被曝光无数次\n
\n',zt={name:"BlockShow",components:{CodeSnippet:B},data:function(){return{trackViewComponentSnippet:Ft,trackViewComponentOnceSnippet:Kt,viewportTrackViewComponentSnippet1:qt,viewportTrackViewComponentSnippet2:Gt,show:!1,rest1:null,rest2:null}},methods:{fetchRest:function(){var t=Object($["a"])(regeneratorRuntime.mark(function t(e){var n;return regeneratorRuntime.wrap(function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,new T.a(function(t){setTimeout(function(){t({data:"success"})},300)});case 2:n=t.sent,!e&&this.rest1===n.data||e&&this.rest2===n.data?this.$message.success("异步事件返回成功,但返回结果和上一次相等,埋点不会上报"):(this.$message.success("异步事件返回成功"),e?this.rest2=n.data:this.rest1=n.data);case 4:case"end":return t.stop()}},t,this)}));function e(e){return t.apply(this,arguments)}return e}()}},Yt=zt,Jt=Object(w["a"])(Yt,It,Lt,!1,null,null,null),Qt=Jt.exports;function Xt(t){return Xt="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Xt(t)}function Zt(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function te(t,e){for(var n=0;n=0||(r[n]=t[n]);return r}function se(t,e){if(null==t)return{};var n,a,r=re(t,e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(r[n]=t[n])}return r}function ie(t,e){return le(t)||pe(t,e)||de()}function ce(t){return oe(t)||ue(t)||ve()}function oe(t){if(Array.isArray(t)){for(var e=0,n=new Array(t.length);e0&&void 0!==arguments[0]?arguments[0]:{};return[Object.values(t),Object.keys(t)]}function ge(t){var e,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:200;return function(){for(var a=this,r=arguments.length,s=new Array(r),i=0;i0&&t.right>0&&t.tope.right||t.top>e.bottom||t.right2&&void 0!==arguments[2]?arguments[2]:window,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1;if(Zt(this,t),!Se(e))throw new Error("not an element node");if(r>1&&r<=0)throw new Error("'percent' must be a number between 0 and 1");this.ele=e,this.ref=n,this.refWin=a,this.started=!1,this.percent=r,this.prevPerc=null,this.listeners={},this.removeScrollLisener=null,this.init()}return ee(t,[{key:"init",value:function(){var t=this;if(!this.started){var e=ge(this.visibilitychange.bind(this));e(),this.removeScrollLisener=function(e){return t.ref?t.ref.$on("scroll",e):(t.refWin.addEventListener("scroll",e,!0),function(){return t.refWin.removeEventListener("scroll",e,!0)})}(e),this.started=!0}}},{key:"viewport",value:function(){var t=this.refWin,e=Se(t)?t.getBoundingClientRect():t;return{top:Se(t)?e.top:0,right:e.right||e.innerWidth,bottom:e.bottom||e.innerHeight,left:e.left||0,height:t.innerHeight||t.offsetHeight,width:t.innerWidth||t.offsetWidth}}},{key:"$on",value:function(t,e){var n=this.listeners[t]||(this.listeners[t]=[]);return n.push(e),this}},{key:"$off",value:function(t,e){if(e){var n,a=this.listeners[t],r=a.length;while(r--)if(n=a[r],n===e||n.cbk===e){a.splice(r,1);break}return this}}},{key:"$once",value:function(t,e){var n=this,a=function a(){n.$off(t,a);for(var r=arguments.length,s=new Array(r),i=0;i1?n-1:0),r=1;rwindow.innerHeight&&(e.bottom=window.innerHeight),e.left<0&&(e.left=0),e.right>window.innerWidth&&(e.right=window.innerWidth),n=t.top>=e.top&&t.bottom>e.bottom?e.bottom-t.top:t.top=e.left&&t.right>e.right?e.right-t.left:t.left=this.percent&&(this.$emit("fullyvisible"),this.prevPerc=r)}},{key:"destroy",value:function(){me(this.removeScrollLisener)&&this.removeScrollLisener()}}]),t}(),je=["async","delay","watch","show","once","custom"];function xe(t,e,n,a){var r=this,s=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};t.$unwatch=a.$watch(function(){return a[e]},function(e,a){e!==a&&(s.immediate||t.contains(r.target))&&n(),r.target=null})}function Ve(t,e,n,a,r,s){var i=this,c=e.value,o=e.arg,l=e.modifiers,u=e.rawName,p=n.context,v=n.componentInstance;if(!s[o])throw new Error("tracking event does not exist");var d=[],m=s[o].bind(null,p),h=function(e,n,a){return xe.call(i,t,e,n,p,a)},f=function(){for(var t=arguments.length,e=new Array(t),n=0;n1?e-1:0),a=1;a68?a-1:a).concat(e)}],M:[He,function(t,e){t.month=e-1}],d:[He,function(t,e){t.day=e}],h:[He,function(t,e){t.hour=e}],m:[He,function(t,e){t.minute=e}],s:[He,function(t,e){t.second=e}]};Fe.MM=Fe.M,Fe.dd=Fe.d,Fe.hh=Fe.h,Fe.H=Fe.h,Fe.HH=Fe.h,Fe.mm=Fe.m,Fe.ss=Fe.s;var Ke=n("a4bb"),qe=n.n(Ke),Ge=function(t){var e=qe()(t).reduce(function(e,n){return e.push({key:n,val:t[n]}),e},[]),n=e.map(function(t){var e=t.key,n=t.val;return'\n
\n '.concat(e,"\n ").concat(n,"\n
\n ")}).join("");return'\n
\n
\n key\n value\n
\n '.concat(n,"\n
\n ")};function ze(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=Object(We["a"])({evt:t},e,{action_time:Le(P()())});"1"===t&&i()("统计UVPV埋点"),"2"===t&&i()({message:"统计页面停留时间埋点",customClass:"message-offset"}),r.a.success({title:"上报数据如下:",dangerouslyUseHTMLString:!0,message:Ge(n),customClass:"notification__large",position:"1"===t||"2"===t?"top-left":"top-right"})}var Ye={UVPV:function(){ze("1")},TONP:function(t,e){var n=e.et,a=e.dt;ze("2",{stt:"".concat((a-n)/1e3,"s")})},18015:function(t){var e=t.$route.name;ze("18015",{source_page:e})},18016:function(t,e,n){var a=t.$route.name,r=e.id,s=n.target;ze("18016",{id:r,source_page:a,target:s})},18017:function(t){var e=t.index,n=t.$route.name;ze("18017",{source_page:n,index:e})},18018:function(t){var e=t.rest,n=t.$route.name;ze("18018",{source_page:n,rest:e})},18019:function(t,e,n){var a=t.$route.name,r=e.id,s=n.target;ze("18019",{id:r,source_page:a,target:s})},18020:function(t){var e=t.activeName,n=t.$route.name;ze("18020",{source_page:n,active_name:e})},18021:function(t){var e=t.rest,n=t.$route.name;ze("18021",{source_page:n,rest:e})},18022:function(t){var e=t.$route.name;ze("18022",{source_page:e,description:"这是一个页面初始化埋点"})},18023:function(t){var e=t.$route.name;ze("18023",{source_page:e})},18025:function(t){var e=t.rest1,n=t.$route.name;ze("18025",{source_page:n,rest:e})},18026:function(t){var e=t.rest2,n=t.$route.name;ze("18026",{source_page:n,rest:e})},18027:function(t){var e=t.$route.name;ze("18027",{source_page:e,description:"我想被曝光无数次"})},18028:function(t){var e=t.$route.name;ze("18028",{source_page:e,description:"我只想被曝光一次"})},18029:function(t){var e=t.$route.name;ze("18029",{source_page:e,description:"我也想被曝光无数次"})},18030:function(t){var e=t.$route.name;ze("18030",{source_page:e,description:"我也想被曝光无数次"})}},Je=Object(We["a"])({},Ye);h["default"].use(f["a"]),h["default"].use(m.a),h["default"].use(v.a),h["default"].use(u.a),h["default"].use(o.a),h["default"].use(Ae,{trackEvents:Je,trackEnable:{UVPV:"routeUpdate",TONP:!0}}),h["default"].prototype.$message=i.a,h["default"].prototype.$notify=r.a;var Qe=new f["a"]({routes:[{path:"/",name:"HOME",component:rt},{path:"/started",name:"STARTED",component:mt},{path:"/custom-events",name:"CUSTOM_EVENTS",component:$t},{path:"/track-view",name:"TRACK_VIEW",component:Ut,meta:{keepAlive:!0}},{path:"/block-show",name:"BLOCK_SHOW",component:Qt},{path:"*",redirect:"/"}]});new h["default"]({el:"#app",router:Qe,render:function(t){return t(b)}})}}); 2 | //# sourceMappingURL=app.efe84ade.js.map --------------------------------------------------------------------------------