openks / learn-vue

自定义组件文档
https://openks.github.io/learn-vue
0 stars 0 forks source link

20191122经验整理 #125

Open openks opened 4 years ago

openks commented 4 years ago

并行加载异步函数

多个异步函数之间没有关联关系 需要等待则需要添加await

async function getFirstVal() {
    this.first = await new Promise((reslove, reject) => {
        try {
            setTimeout(function () {
                console.log("getFirstVal")
                reslove("1")
            }, 1000)
        } catch (error) {
            reject(error)
        }
    })
}

async function getSecondVal() {
    this.first = await new Promise((reslove, reject) => {
        try {
            setTimeout(function () {
                console.log("getSecondVal")
                reslove("2")
            }, 1000)
        } catch (error) {
            reject(error)
        }
    })
}

async function  getOtherVal() {
    this.first = await new Promise((reslove, reject) => {
        try {
            setTimeout(function () {
                console.log("getOtherVal")
                reslove("3")
            }, 1000)
        } catch (error) {
            reject(error)
        }
    })
}

async function  doSomeWork() {
    // 等待第一个函数执行后,第二和第三同步执行
 await this.getFirstVal()
 this.getSecondVal()
 this.getOtherVal()
}
doSomeWork()

控制台包 NavigationDupliacted 解决方案

引用vue-router的页面添加如下代码

const originalPush = Router.prototype.push
Router.prototype.push = function push(location:string) {
  return (originalPush.call(this, location) as any).catch((err:any) => err)
}

integrity checksum failed when using sha512: wanted sha512 错误解决

问题描述 本地开发一切正常,在服务器上执行npm install命令安装依赖时报错integrity checksum failed when using sha512: wanted sha512

解决方案: 删除本地package-lock.json文件并移除node-modules模块重新执行npm install 本地打包没问题后提交服务器,服务器上重新安装依赖即可

typeScript单文件组件使用vuex

详见:vuex-class文档

import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}

class格式单文件组件使用mixin

详见:vue-class-component文档

// mixin.js
import Vue from 'vue'
import Component from 'vue-class-component'

// You can declare a mixin as the same style as components.
@Component
export default class MyMixin extends Vue {
  mixinValue = 'Hello'
}
// 引用文件 
import Component, { mixins } from 'vue-class-component'
import MyMixin from './mixin.js'

// Use `mixins` helper function instead of `Vue`.
// `mixins` can receive any number of arguments.
@Component
export class MyComp extends mixins(MyMixin) {
  created () {
    console.log(this.mixinValue) // -> Hello
  }
}

vue typescript 项目使用cdn方式引入调用 this.$message时报错问题

具体报错信息如下

Property '$message' does not exist on type 'Single Vue Component Name'

使用cdn方式引入使用方式为

//element会挂载在window对象上
window.ELEMNET.Message("这里是提示信息")

vue typescript 项目同时使用element和vant是出现声明冲突问题

直接找到node_modules相关文件移除声明或者修改证明为any类型即可

method 方法调用filter

方法一 直接引入filter文件当方法调用即可

方法二

this.$options.filters['filterName']

提交时确保所有文件均已上传

1、beforeuploadtotleUploadFile+1
2、on-remove且状态为uploadingtotleUploadFile-1
3、uploadsuccesstotleDealedFile+1
4、uploaderrortotleDealedFile+1
5、totleUploadFile===totleDealedFile
当满足以上所有条件时所有文件均已上传

npm 命令获取其他参数

npm start 1 2 3 4
process.argv.slic(2)
//[1,2,3,4]

vuecli 项目使用mock数据方案

添加其他环境变量

判断环境为开发环境且启用mock(通过其他环境变量定义即可)则返回mock的url

// mixin.js
getMockUrl(url: string) {
    let str = url
    if (process.env.NODE_ENV === "development" && process.env.VUE_APP_MOCK === "true") {
        str = '/mock' + url
    }
    return str
}
// src/mock/index.js

const Mock = requir('mockjs')
let user = {
    name: "@cname",
    id: "@id",
}
exports.ab = Mock.mock(user)
// vue.config.js
let data = require('@/mock/index.js')
devServer:{
    before(app){
        const str = "/mock"
        app.use(str+"/a/b",(req,res,next)=>{
            res.json(data.ab)
        })
    }
}
// 引用文件
let url ="/user/getUserById"
// 注释使用开发环境配置的地址
// 打开使用mock数据
url = this.getMockUrl(url)
let params = {
    id:123
}
this.axios.post(url,params)

typescript 定义promise返回值类型

// promise 返回值string
 readFile(file:File){
     return new Promise<string>((resolve,reject)=>{
         let reader = new FileReader()
         reader.readAsDataURL(file)
         reader.onload = function(e:any){
             let base64url:string = e.target.result
             resolve(base64url) 
         }
     })
 }

vue插件编写

// 定义插件文件
var utils = {}
utils.install = function (Vue, options) {
  var util = {}
  util.getBirthdayById = (idStr) => {
    let tmp = '1970101'
    if (idStr === undefined) {
      return tmp
    }
    if (idStr.length === 18) {
      tmp = idStr.substr(6, 8)
    }else if (idStr.length === 15) {
      tmp = '19' + idStr.substr(6, 6)
      return tmp
    }
  }
  util.getGanderById = (idStr) => {
    let tmp = 1
    if (idStr.length === 18) {
      tmp = idStr.substr(16, 1)
    }else {
      tmp = idStr.substr(14, 1)
    }
    return tmp % 2 === 1 ? '男' : '女'
  }
  // 此处的Vue即Vue实例
  Vue.prototype.util=util
}
export default utils

// 使用自定义插件
import utils from "@/utils/index.js"
Vue.use(utils)

elementUI upload组件上传失败但仍然显示成功问题解决

upload组件判断成功标志位http code 为200即显示成功
但后台逻辑可能并非如此,上传失败状态码依旧为200在其他信息里包含错误信息

解决方案
判断返回逻辑如果为上传文件失败则给出失败的提示信息并移除页面展示成功但实际并未上传成功的文件

//处理上传成功事件 判断是否真的上传成功
handerUplaodSuccess(response: any, file: File, fileList: FileList) {
    if (response.data.code !== "successCode") {
        this.$messgae(response.data.message)
        this.handerRemove(file, fileList)
    }
}
// 处理上传失败事件
handerRemove(file: File, fileList: FileList) {
    //  通过ref获取组件引用
    let upload: any = this.$refs.upload
    let uploads:Array<any> = upload.uploadFiles
    let index = uploads.findIndex(ele => ele.uid === file.uid)
    if (index > -1) {
        uploads = uploads.splice(index, 1)
    }
}

在axios的封装文件及store里调用element-ui的方法

在封装文件及store里无法获取vue实例,可以通过UI库绑定到原型链上的方法调用弹框之类的组件

Vue.prototype.$message.error(error.message)

VScode编辑器正则

在行首插入字符串 ^替换为要插入的字符串即可
在行尾插入字符串 \n替换为要插入的字符串\n即可
删除空行 ^\s*(?=\r?$)\n替换为空即可

把swagger-ui的文档替换为字段名和字段说明的方法

1.从页面复制字段名和字段值,到文件里会出现换行情况
2.把字段名和字段解释放到同一行,并对字段名添加后引号,对字段解释添加前引号
复制\n替换为':'
3.移除多余的空行 ^\s*(?=\r?$)\n替换为空即可
4.添加行尾字段解释引号和逗号 \n替换为',\n
5.添加行首字段名引号 ^替换为'

nightwatch 模拟与服务器交互

e2e测试不需要与服务器进行任何交互,只需要等待 服务器失败就会出现测试不通过情况

nightwatch 选择列表里的某个元素

// 点击第一个类为el-checkbox的元素
.click({
    selector:".el-checkbox",
    index: 0,
    suppressNotFoundErrors: true,
})

图片上传前循环压缩直至达到大小要求

  1. 读取选中文件到文件流,返回base64字符串--此过程为异步操作
  2. 加载base64图片,加载完成后返回Image对象,此时可以获取图片宽高---此过程为异步操作
  3. 绘制画板,把图片绘制到画板上,转换为blob
  4. blob转换为新文件 5.如果新文件过大则循环执行以上步骤
// 异步读文件
readFile(file: File) {
    return new Promise<string>((resolve, reject) => {
        let reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = function (e: any) {
            let base64url: string = e.target.result
            resolve(base64url)
        }
    })
}
//加载图片
loadImage(url: string) {
    return new Promise<HTMLImageElement>((resolve, reject) => {
        let img: any = new Image()
        img.src = url
        img.onload = () => {
            resolve(img)
        }
    })
}

// 压缩图片
compressImage(file:File){
    return new Promise(async(resolve,reject)=>{
        let imgSize = file.size
        let allowedSize =1024*200 //200K
        if(imgSize>allowedSize){
            let canvas = document.createElement("canvas")
            let content:any  = canvas.getContext("2d")
            let url = await this.readFile(file)
            let img:any = await this.loadImage(url)
            canvas.width = img.width *0.6
            canvas.height = img.height* 0.6
            content.drawImage(img,0,0,canvas.width,canvas.height)
            canvas.toBlob(async(blob:any)=>{
                let nf = new File([blob],file.name,{type:file.type})
                while(nf.size>allowedSize){
                    let newfile:any = await this.compressImage(nf)
                    if(newfile.size<=allowedSize){
                        resolve(newfile)
                        return 
                    }
                }
                if(nf.size<=allowedSize){
                    resolve(nf)
                    return 
                }
            },file.type,0.8)
        }else{
            resolve(file)
        }
    })
}

网页展示及打印PDF

使用vue-pdf插件进行PDF预览及打印

<template>
  <div>
    <div v-for="i in files"
         :key="i.id">
      <pdf :pages="p"
           v-for="p in i.numPages"
           :key="p"
           :src="i.src"
           ref="pdfRef"></pdf>
    </div>
  </div>
</template>

<script lang='ts'>
import pdf from "vue-pdf";
import CMapReaderFactory from "vue-pdf/src/CMapReaderFactory.js";
import {
  Component,
  Vue,
  Prop,
  PropSync,
  Emit,
  Watch
} from "vue-property-decorator";

@Component({
  components: {
    pdf
  }
})
export default class Test extends Vue {
  files = [] as Array<any>;

  handerPrint() {
    let ref: any = this.$refs.pdfRef;
    if (ref !== undefined) {
      //  打印第一份文件
      ref[0].print();
    }
  }
  dealPdf(item: any) {
    let pdfItem = item;
    pdfItem.numPages = undefined;
    pdfItem.src = pdf.createLoadingTask({
      url: item.url,
      CMapReaderFactory
    });
    pdfItem.src.then(
      (pdfs: any) => {
        pdfItem.numPages = pdfs.numPages;
        this.files = [pdfItem];
      },
      (error: any) => {
        this.files = [];
        console.error(error);
      }
    );
  }
  mounted() {
    this.dealPdf({ url: "http://www.xxx.com" });
  }
}
</script>

element-ui 上传图片预览使用插件

使用v-viewer插件进行图片预览

<template>
  <div>
    <viewer :images="fileList">
      <img v-for="item in fileList"
           :src="item.response.result.url"
           :id="item.uid"
           alt=""
           :ref="item.uid"
           :key="item.uid">
    </viewer>
  </div>
</template>

<script lang='ts'>
//index.ts 文件使用插件
import Viewer from "v-viewer";
import "viewerjs/dist/viewer.css";

Vue.use(Viewer, {
  defaultOptions: {
    button: false,
    navbar: false,
    title: false,
    toobar: true
  }
});
// 调用Vue文件
import {
  Component,
  Vue,
} from "vue-property-decorator";

@Component
export default class Test extends Vue {
  fileList = [] as Array<any>;

  handerPreview(file: any) {
    if (file.status === "uploading") {
      this.$message.error("文件正在上传中,请稍后");
      return false;
    }
    let ref: Array<HTMLImageElement> | undefined = this.refs[file.uid];
    if (ref.length === 1) {
      ref[0].click();
    }
  }
  //回填原来已上传的数据
  async getFiles() {
    let url = "";
    let params = {};
    let res = await this.axios.post(url, params);
    if (res.data.code === "OK") {
      this.fileList = res.data.result.map((ele: any) => {
        return {
          response: {
            result: ele
          },
          url: ele.url,
          uid: ele.uid
        };
      });
    }
  }
}
</script>

移动端手势密码

移动端手势密码

<template>
  <div class="masks" v-show="currentValue">
    <div class="gesturePwd">
      <div class="box">
        <h4 ref="gestureTitle" class="gestureTitle">请绘制您的图形密码</h4>
        <a class="reset" ref="updatePassword" @click="updatePassword()">重置密码</a>
        <a class="close" ref="updatePassword" @click="closePwd(false)">关闭</a>
        <canvas ref="canvas"></canvas>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    props: {
      value: {
        type: Boolean,
        default: false
      },
    },
    data() {
      return {
        currentValue: false,
        ctx: '',
        width: 0,
        height: 0,
        devicePixelRatio: 0,
        chooseType: '',
        r: '',// 公式计算
        lastPoint: [],
        arr: [],
        restPoint: [],
        pswObj: {step: 2},
        canvas: ''
      }
    },
    watch: {
      value: {
        handler: function (val) {
          this.currentValue = val
        },
        immediate: true
      },
      currentValue(val) {
        this.$emit(val ? 'on-show' : 'on-hide')
        this.$emit('input', val)
      }
    },
    created() {
      if (typeof this.value !== 'undefined') {
        this.currentValue = this.value
      }
    },
    mounted() {
      this.setChooseType(3);
    },
    methods: {
      closePwd(bol) {
        this.$emit("handPwd",bol);
        this.currentValue = false;
      },
      drawCle(x, y) { // 初始化解锁密码面板 小圆圈
        this.ctx.strokeStyle = '#87888a';//密码的点点默认的颜色
        this.ctx.lineWidth = 2;
        this.ctx.beginPath();
        this.ctx.arc(x, y, this.r, 0, Math.PI * 2, true);
        this.ctx.closePath();
        this.ctx.stroke();
      },
      drawPoint(style) { // 初始化圆心
        for (var i = 0; i < this.lastPoint.length; i++) {
          this.ctx.fillStyle = style;
          this.ctx.beginPath();
          this.ctx.arc(this.lastPoint[i].x, this.lastPoint[i].y, this.r / 2.5, 0, Math.PI * 2, true);
          this.ctx.closePath();
          this.ctx.fill();
        }
      },
      drawStatusPoint(type) { // 初始化状态线条
        for (var i = 0; i < this.lastPoint.length; i++) {
          this.ctx.strokeStyle = type;
          this.ctx.beginPath();
          this.ctx.arc(this.lastPoint[i].x, this.lastPoint[i].y, this.r, 0, Math.PI * 2, true);
          this.ctx.closePath();
          this.ctx.stroke();
        }
      },
      drawLine(style, po, lastPoint) {//style:颜色 解锁轨迹
        this.ctx.beginPath();
        this.ctx.strokeStyle = style;
        this.ctx.lineWidth = 3;
        this.ctx.moveTo(this.lastPoint[0].x, this.lastPoint[0].y);

        for (var i = 1; i < this.lastPoint.length; i++) {
          this.ctx.lineTo(this.lastPoint[i].x, this.lastPoint[i].y);
        }
        this.ctx.lineTo(po.x, po.y);
        this.ctx.stroke();
        this.ctx.closePath();

      },
      createCircle() {// 创建解锁点的坐标,根据canvas的大小来平均分配半径
        var n = this.chooseType;
        var count = 0;
        this.r = this.ctx.canvas.width / (2 + 4 * n);// 公式计算
        this.lastPoint = [];
        this.arr = [];
        this.restPoint = [];
        var r = this.r;
        for (var i = 0; i < n; i++) {
          for (var j = 0; j < n; j++) {
            count++;
            var obj = {
              x: j * 4 * r + 3 * r,
              y: i * 4 * r + 3 * r,
              index: count
            };
            this.arr.push(obj);
            this.restPoint.push(obj);
          }
        }
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
        for (var i = 0; i < this.arr.length; i++) {
          this.drawCle(this.arr[i].x, this.arr[i].y);

        }
      },
      getPosition(e) {// 获取touch点相对于canvas的坐标
        var rect = e.currentTarget.getBoundingClientRect();
        var po = {
          x: (e.touches[0].clientX - rect.left) * this.devicePixelRatio,
          y: (e.touches[0].clientY - rect.top) * this.devicePixelRatio
        };
        return po;
      },
      update(po) {// 核心变换方法在touchmove时候调用
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
        for (var i = 0; i < this.arr.length; i++) { // 每帧先把面板画出来
          this.drawCle(this.arr[i].x, this.arr[i].y);
        }
        this.drawPoint('#27AED5');// 每帧花轨迹
        this.drawStatusPoint('#27AED5');// 每帧花轨迹
        this.drawLine('#27AED5', po, this.lastPoint);// 每帧画圆心
        for (var i = 0; i < this.restPoint.length; i++) {
          if (Math.abs(po.x - this.restPoint[i].x) < this.r && Math.abs(po.y - this.restPoint[i].y) < this.r) {
            this.drawPoint(this.restPoint[i].x, this.restPoint[i].y);
            this.lastPoint.push(this.restPoint[i]);
            this.restPoint.splice(i, 1);
            break;
          }
        }
      },
      checkPass(psw1, psw2) {// 检测密码
        var p1 = '', p2 = '';
        for (var i = 0; i < psw1.length; i++) {
          p1 += psw1[i].index + psw1[i].index;
        }
        for (var i = 0; i < psw2.length; i++) {
          p2 += psw2[i].index + psw2[i].index;
        }
        return p1 === p2;
      },
      storePass(psw) {// touchend结束之后对密码和状态的处理
        if (this.pswObj.step == 1) {
          if (this.checkPass(this.pswObj.fpassword, psw)) {
            this.pswObj.step = 2;
            this.pswObj.spassword = psw;
            this.$refs.gestureTitle.innerHTML = '密码保存成功';
            this.drawStatusPoint('#2CFF26');
            this.drawPoint('#2CFF26');
            window.localStorage.setItem('passwordxx', JSON.stringify(this.pswObj.spassword));
            window.localStorage.setItem('chooseType', this.chooseType);
          } else {
            this.$refs.gestureTitle.innerHTML = '两次不一致,重新输入';
            this.drawStatusPoint('red');
            this.drawPoint('red');
            delete this.pswObj.step;
          }
        } else if (this.pswObj.step == 2) {
          if (this.checkPass(this.pswObj.spassword, psw)) {
            var gestureTitle = this.$refs.gestureTitle;
            gestureTitle.style.color = "#2CFF26";
            gestureTitle.innerHTML = '解锁成功';
            this.drawStatusPoint('#2CFF26');//小点点外圈高亮
            this.drawPoint('#2CFF26');
            this.drawLine('#2CFF26', this.lastPoint[this.lastPoint.length - 1], this.lastPoint);// 每帧画圆心
            this.closePwd(true);
          } else if (psw.length < 4) {
            this.drawStatusPoint('red');
            this.drawPoint('red');
            this.drawLine('red', this.lastPoint[this.lastPoint.length - 1], this.lastPoint);// 每帧画圆心
            var gestureTitle = this.$refs.gestureTitle;
            gestureTitle.style.color = "red";
            gestureTitle.innerHTML = '请连接4个点';

          } else {
            this.drawStatusPoint('red');
            this.drawPoint('red');
            this.drawLine('red', this.lastPoint[this.lastPoint.length - 1], this.lastPoint);// 每帧画圆心
            var gestureTitle = this.$refs.gestureTitle;
            gestureTitle.style.color = "red";
            gestureTitle.innerHTML = '密码错误';
          }
        } else {
          this.pswObj.step = 1;
          this.pswObj.fpassword = psw;
          this.$refs.gestureTitle.innerHTML = '再次输入';
        }
      },
      makeState() {
        if (this.pswObj.step == 2) {
          this.$refs.updatePassword.style.display = 'block';
          var gestureTitle = this.$refs.gestureTitle;
          gestureTitle.style.color = "#87888a";
          gestureTitle.innerHTML = '请解锁';
        } else if (this.pswObj.step == 1) {
          this.$refs.updatePassword.style.display = 'none';
        } else {
          this.$refs.updatePassword.style.display = 'block';
        }
      },
      setChooseType(type) {
        this.chooseType = type;
        this.init();
      },
      updatePassword() {
        window.localStorage.removeItem('passwordxx');
        window.localStorage.removeItem('chooseType');
        this.pswObj = {};
        this.$refs.gestureTitle.innerHTML = '绘制解锁图案';
        this.reset();
      },
      initDom() {
        this.chooseType = Number(window.localStorage.getItem('chooseType')) || 3;
        this.devicePixelRatio = window.devicePixelRatio || 1;
        var canvas = this.$refs.canvas;
        var width = this.width || 320;
        var height = this.height || 320;
        // 高清屏锁放
        canvas.style.width = width + "px";
        canvas.style.height = height + "px";
        canvas.height = height * this.devicePixelRatio;
        canvas.width = width * this.devicePixelRatio;
      },
      init() {
        this.initDom();
        this.pswObj = window.localStorage.getItem('passwordxx') ? {
          step: 2,
          spassword: JSON.parse(window.localStorage.getItem('passwordxx'))
        } : {};
        this.lastPoint = [];
        this.makeState();
        this.touchFlag = false;
        this.canvas = this.$refs.canvas;
        this.ctx = this.canvas.getContext('2d');
        this.createCircle();
        this.bindEvent();
      },
      reset() {
        this.makeState();
        this.createCircle();
      },
      bindEvent() {
        var self = this;
        this.canvas = this.$refs.canvas;
        this.canvas.addEventListener("touchstart", function (e) {
          e.preventDefault();// 某些android 的 touchmove不宜触发 所以增加此行代码
          var po = self.getPosition(e);
          for (var i = 0; i < self.arr.length; i++) {
            if (Math.abs(po.x - self.arr[i].x) < self.r && Math.abs(po.y - self.arr[i].y) < self.r) {
              self.touchFlag = true;
              self.drawPoint(self.arr[i].x, self.arr[i].y);
              self.lastPoint.push(self.arr[i]);
              self.restPoint.splice(i, 1);
              break;
            }
          }
        }, false);
        this.canvas.addEventListener("touchmove", function (e) {
          if (self.touchFlag) {
            self.update(self.getPosition(e));
          }
        }, false);
        this.canvas.addEventListener("touchend", function (e) {
          if (self.touchFlag) {
            self.touchFlag = false;
            self.storePass(self.lastPoint);
            setTimeout(function () {
              self.reset();
            }, 1000);
          }
        }, false);
      }
    }
  }
</script>

<style scoped>
  .masks {
    text-align: center;
    position: fixed;
    z-index: 1000;
    top: 0;
    right: 0;
    left: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.6);
  }
  .gesturePwd {
    position: fixed;
    z-index: 5000;
    width: 100%;
    height: 100%;
    top: 50%;
    left: 50%;
    -webkit-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
    text-align: center;
    border-radius: 3px;
    overflow: hidden;
    background-color: #000;
  }
  .gestureTitle {
    color: #87888a;
    margin-top: 85px;
    font-size: 20px;
    font-weight: normal;
  }
  .box{
    position: absolute;
    top:0;
    left:0;
    right:0;
    bottom:0;
  }
  .box a{
    position: absolute;
    top: 5px;
    color:#fff;
    font-size: 13px;
    display:block;
  }
  a.reset{
    left: 5px;
  }
  a.close{
    right :5px;
  }
  .box canvas{
    background-color: #000;
    display: inline-block;
    margin-top: 76px;
    width: 320px;
    height: 320px;
  }
</style>
<!-- 组件调用 -->
<template>
  <div class="hello">
    <button @click="showClicked" style="width:90px;height:50px;font-size:16px;background-color:#eee">手势密码</button>
    <pwd v-model="showPwd" @handPwd="handPwd"></pwd>
  </div>
</template>

<script>
  import pwd from '@/components/pwd'
  export default {
    name: 'hello',
    data() {
      return {
        showPwd: false
      }
    },
    methods: {
      showClicked() {
        this.showPwd = true;
      },
      handPwd(val) {
        console.log(val);
      }
    },
    components: {
      pwd
    },
  }
</script>

typescript vue单文件组件路有钩子不生效

详见vue-class-component

// main.ts 注册路有钩子
import Component from 'vue-class-component'

// Register the router hooks with their names
Component.registerHooks([
  'beforeRouteEnter',
  'beforeRouteLeave',
  'beforeRouteUpdate' // for vue-router 2.2+
])