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

相关推荐
前端小巷子34 分钟前
Vue 自定义指令
前端·vue.js·面试
Stringzhua1 小时前
Vue中的数据渲染【4】
css·vue.js·css3
草梅友仁12 小时前
草梅 Auth 1.4.0 发布与 ESLint v9 更新 | 2025 年第 33 周草梅周报
vue.js·github·nuxt.js
萌萌哒草头将军13 小时前
Oxc 最新 Transformer Alpha 功能速览! 🚀🚀🚀
前端·javascript·vue.js
武昌库里写JAVA13 小时前
JAVA面试汇总(四)JVM(一)
java·vue.js·spring boot·sql·学习
littleding15 小时前
Vue3之计算属性
前端·vue.js
Juchecar15 小时前
采用 Vue 3 实现单页应用(SPA)与本地数据存储方案
前端·javascript·vue.js
congvee17 小时前
vue学习第3期 - 集成UI库
vue.js
前端小巷子19 小时前
Vue 事件绑定机制
前端·vue.js·面试
伍哥的传说19 小时前
Vue 3.5重磅更新:响应式Props解构,让组件开发更简洁高效
前端·javascript·vue.js·defineprops·vue 3.5·响应式props解构·vue.js新特性