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

相关推荐
_codeOH1 天前
Vue 3 vs React 19:框架还在卷,核心原理就这些
前端·vue.js
英勇无比的消炎药1 天前
新手必看玩转TinyRobot一定要避开这些坑
前端·vue.js
英勇无比的消炎药1 天前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
英勇无比的消炎药1 天前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药1 天前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo1 天前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰1 天前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·1 天前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
rising start1 天前
九、vue3 组件通信:全场景详解
前端·vue.js·typescript
编程技术手记1 天前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js