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 用来自定义弹窗布局,因此在这个案例中只能强行覆盖原有组件的样式实现全屏弹窗,可能在实际项目中样式会出现一些问题,需要自行调整。

相关推荐
拉不动的猪2 小时前
简单回顾下Weakmap在vue中为何不能去作为循环数据源,以及替代方案
前端·javascript·vue.js
java水泥工4 小时前
校园管理系统|基于SpringBoot和Vue的校园管理系统(源码+数据库+文档)
数据库·vue.js·spring boot
正义的大古6 小时前
OpenLayers常用控件 -- 章节七:测量工具控件教程
前端·javascript·vue.js·openlayers
子兮曰8 小时前
🚀Vue3异步组件:90%开发者不知道的性能陷阱与2025最佳实践
前端·vue.js·vite
kk不中嘞10 小时前
浅谈前端框架
前端·vue.js·react.js·前端框架
影子信息11 小时前
el-tree 点击父节点无效,只能选中子节点
前端·javascript·vue.js
徐小夕11 小时前
用Vue3写了一款协同文档编辑器,效果简直牛!
前端·javascript·vue.js
小菜全12 小时前
打包 Uniapp
javascript·vue.js·html5
小高00715 小时前
🔥🔥🔥Vue部署踩坑全记录:publicPath和base到底啥区别?99%的前端都搞错过!
前端·vue.js·面试