Vant4 Vue3 实现横屏签名框

在界面中使用 van-signature 组件在移动端界面下过小,这就需要把组件调整为全屏横屏。为了不干扰原有布局,所以这里结合 van-popup 组件,以弹窗的形式实现横屏签名框。

思路概要

起初,为了让布局能够全屏,我使用 CSS 进行调整,将弹窗扩展到全屏,然后再使用 transform 属性将 van-signature 组件旋转 90 度到横屏。

但是,这样做会导致 canvas 绘制错位,如果采用这种方法,就需要再对 canvas 进行一次转换,反倒更复杂了。

所以,我们可以换个思路,让画布保持竖屏,直接把 canvas 铺满布局,在绘制好签名后,获取到 base64 图片,然后逆时针旋转 90 度得到横屏图片。这样看起来,就和横屏绘制的效果是一样的了。

关键方法

那么如何逆时针旋转绘制好的图片呢?同样使用 canvas 来完成,我们只需要创建一个画布,将图片放进去,使用 ctx.rotate(-Math.PI / 2); 旋转一下再导出为 base64 图片就可以了。

javascript 复制代码
async function rotateToBase64File(
  base64: string,
  mimeType: string = "image/png"
): Promise<string> {
  const img = new Image();
  img.src = base64;

  await new Promise<void>((resolve, reject) => {
    img.onload = () => resolve();
    img.onerror = (e) => reject(new Error("图片加载失败"));
  });

  const canvas = document.createElement("canvas");
  canvas.width = img.height;
  canvas.height = img.width;

  const ctx = canvas.getContext("2d");
  if (!ctx) throw new Error("无法获取画布上下文");

  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.rotate(-Math.PI / 2); // 逆时针旋转90度
  ctx.drawImage(img, -img.width / 2, -img.height / 2);

  return canvas.toDataURL(mimeType);
}

签名组件封装和使用

这里简单封装一个签名组件,需要传入一个 signaturePic 作为 v-model 参数,用于和父组件共享。

javascript 复制代码
<template>
  <div>
    <!-- 签名空白区域 -->
    <div class="signature-empty" v-if="!signaturePic" @click="toSign">
      点击此处进行签名
    </div>

    <!-- 签名预览区域 -->
    <div v-else class="signature-preview">
      <div class="preview-header">
        <p>签名预览:</p>
        <button @click="reSign">重签</button>
      </div>
      <van-image class="preview-image" :src="signaturePic" />
    </div>

    <!-- 签名弹窗 -->
    <van-popup v-model:show="showSignature" class="signature-popup">
      <div class="popup-wrapper">
        <div class="popup-rotated">
          <van-signature
            ref="signatureRef"
            class="signature-content"
            @submit="submitSign"
            @cancel="clearSign"
          />
        </div>
      </div>
    </van-popup>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

const signatureRef = ref();
const showSignature = ref<boolean>(false);
const signaturePic = defineModel<string>("signaturePic", { required: true });

// 显示签名
function toSign() {
  showSignature.value = true;
}

// 取消签名
function clearSign() {
  signaturePic.value = "";
  showSignature.value = false;
}

// 重置签名
function reSign() {
  signaturePic.value = "";
  signatureRef.value.clear();
  showSignature.value = true;
}

// 完成签名
async function submitSign(data: { image: string }) {
  if (!data.image) {
    return;
  }

  const base64 = await rotateToBase64File(data.image);
  signaturePic.value = base64;
  showSignature.value = false;
}

// 将 Base64 图片逆时针旋转90度
async function rotateToBase64File(
  base64: string,
  mimeType: string = "image/png"
): Promise<string> {
  const img = new Image();
  img.src = base64;

  await new Promise<void>((resolve, reject) => {
    img.onload = () => resolve();
    img.onerror = (e) => reject(new Error("图片加载失败"));
  });

  const canvas = document.createElement("canvas");
  canvas.width = img.height;
  canvas.height = img.width;

  const ctx = canvas.getContext("2d");
  if (!ctx) throw new Error("无法获取画布上下文");

  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.rotate(-Math.PI / 2); // 逆时针旋转90度
  ctx.drawImage(img, -img.width / 2, -img.height / 2);

  return canvas.toDataURL(mimeType);
}
</script>

<style lang="scss" scoped>
.signature-empty {
  width: 100%;
  height: 200px;
  margin: 10px 0;
  border: 1px dashed #ebebebff;
  border-radius: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #afafaf;
  font-size: 14px;
  cursor: pointer;
  background-color: #ffffff;
}

.preview-header {
  display: flex;
  justify-content: space-between;
  font-size: 14px;
  align-items: center;
  color: #000000;
  margin-bottom: 10px;

  button {
    border: none;
    border-radius: 50px;
    padding: 5px 15px;
    color: #ffffff;
    background-color: #0073ff;
    font-size: 12px;
    cursor: pointer;
  }
}

.preview-image {
  background-color: #ffffff;
  padding: 10px;
  border-radius: 10px;
  box-sizing: border-box;
}

.signature-popup {
  width: 100%;
  height: 100%;
  margin: 0;
}

:deep(.van-popup--center) {
  max-width: 100%;
}

.popup-wrapper {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: row;
  background-color: #f0f0f0;
}

.van-signature.signature-content {
  --van-signature-content-border: 1px dashed #ebebebff;
  --van-signature-padding: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  width: 100%;
  height: 100%;

  :deep(.van-signature__content) {
    min-height: 95vh;
    width: 80vw;
    flex: 1;
  }

  :deep(.van-signature__footer) {
    height: 50px;
    width: 60px;
    transform: rotate(90deg);
    transform-origin: center;
    justify-content: center;

    .van-button__content {
      width: 100px;
    }
  }
}
</style>

在父组件中使用,并且传入 v-model 参数

javascript 复制代码
<template>
  <main>
    <Signature v-model:signature-pic="signaturePic" />
  </main>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Signature from "./components/Signature.vue";

const signaturePic = ref<string>("");
</script>

<style lang="scss" scoped></style>

效果预览

结语

需要注意的是 Vant4 中的 van-signature 只能在移动端下绘制,如果是在网页端就会无法绘制。也许,后续应该寻找一个同时支持网页和移动端的,更好替代方案,或者直接使用纯 canvas 实现保证更好的兼容性。

此外 Vant4 弹窗组件的可塑性不强,似乎没有像其他流行组件库中的设计那样,保留常见的 slot 用来自定义弹窗布局,因此在这个案例中只能强行覆盖原有组件的样式实现全屏弹窗,可能在实际项目中样式会出现一些问题,需要自行调整。

相关推荐
JSON_L3 小时前
Vue rem回顾
前端·javascript·vue.js
为什么名字不能重复呢?6 小时前
Day1||Vue指令学习
前端·vue.js·学习
whhhhhhhhhw7 小时前
Vue3.6 无虚拟DOM模式
前端·javascript·vue.js
Dolphin_海豚8 小时前
vue-vapor 的 IR 是个啥
前端·vue.js·vapor
程序猿小D8 小时前
基于SpringBoot+MyBatis+MySQL+VUE实现的医疗挂号管理系统(附源码+数据库+毕业论文+答辩PPT+项目部署视频教程+项目所需软件工具)
数据库·vue.js·spring boot·mysql·毕业设计·mybatis·医疗挂号管理系统
清风细雨_林木木9 小时前
Vuex 的语法“...mapActions([‘login‘]) ”是用于在组件中映射 Vuex 的 actions 方法
前端·javascript·vue.js
索西引擎10 小时前
浅谈 Vue 的双向数据绑定
前端·vue.js
CodeTransfer10 小时前
今天给大家搬运的是四角线框hover效果
前端·vue.js
用户8417948145613 小时前
vue vxe-table grid 通过配置 ajax 方式自动请求数据,适用于简单场景的列表
vue.js