在景区导览、电商分享、活动引流等各类移动端 H5 场景中,二维码生成与长按保存是提升用户体验的核心功能。本文以通用型二维码页面为例,深度拆解 Vue3 生态下实现「自适应二维码渲染(含背景 / 无背景双模式)」「跨端长按下载」「响应式布局适配」的完整方案,结合核心代码片段与设计思路,兼顾技术实现与用户体验优化,可直接复用至各类业务场景。

一、需求背景与核心设计原则

1. 场景痛点分析

通用型二维码页面需解决四大核心问题:

  • 多设备适配:从千元机到旗舰机,不同屏幕尺寸下二维码需保证扫码清晰度与视觉比例;
  • 跨端交互:移动端触摸长按、PC 端鼠标长按需实现统一的下载触发逻辑;
  • 视觉灵活度:支持「带背景图」和「无背景图」两种模式,适配不同业务视觉需求;
  • 容错兜底:关键功能添加异常捕获,避免单一环节失败导致页面不可用。

2. 核心设计原则

  • 「适配优先」:所有元素尺寸基于视口动态计算,摒弃固定像素值;
  • 「交互统一」:模拟原生 APP 长按体验,抹平端侧操作差异;
  • 「模式灵活」:通过配置项快速切换有无背景图模式,无需重构布局;
  • 「容错兜底」:关键环节添加校验与降级方案,保证页面稳定性。

二、布局层实现:双模式视觉与层叠设计

1. 基础布局(兼容有无背景图)

通过配置项控制背景图展示,核心布局逻辑兼容两种模式:

css

/* 全屏容器:基础层叠结构,兼容有无背景图 */
.top {
  width: 100vw;
  height: 100vh;
  position: relative;
  overflow: hidden; /* 防止子元素溢出 */
  /* 无背景图时的默认底色 */
  background-color: #f8f8f8;
}

/* 背景图模式:动态控制显示 */
.top.has-bg {
  background-image: url("../assets/common-bg.jpg");
  background-size: cover; /* 背景图等比缩放覆盖容器 */
  background-position: center; /* 背景图居中,避免关键区域裁剪 */
  background-repeat: no-repeat;
}

/* 悬浮导航栏:通用轻量风格,适配多场景 */
.header {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 15px;
  background-color: rgba(255, 255, 255, 0.9); /* 半透白底,兼顾可读性 */
  position: relative;
  height: 44px; /* 符合移动端导航栏常规高度 */
  z-index: 10; /* 保证导航栏在最上层 */
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* 轻微阴影提升层次感 */
}

/* 圆形返回按钮:通用交互样式 */
.return {
  position: absolute;
  left: 15px;
  border-radius: 50%;
  background-color: #000;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent; /* 移除移动端点击高亮 */
  transition: background-color 0.2s ease;
}

.return:active {
  background-color: #333; /* 按下反馈 */
}

/* 二维码容器:绝对定位居中,规避导航栏遮挡 */
.qrcode-wrapper {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, 20%); /* 垂直偏移,适配不同屏幕 */
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 10px;
}

/* 二维码画布:通用样式,保证扫码对比度 */
.qrcode-canvas {
  width: 100%;
  height: 100%;
  background-color: #fff; /* 无背景图时的二维码底色 */
  border-radius: 16px; /* 大圆角贴合移动端设计 */
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); /* 阴影增加立体感 */
}

2. 模板层:通过 Props 控制背景模式

html

预览

<template>
  <!-- 动态添加has-bg类控制背景图显示 -->
  <div class="top" :class="{ 'has-bg': showBg }">
    <!-- 通用导航栏 -->
    <div class="header">
      <div class="return" @click="handleGoBack">
        <img src="../assets/left-white.png" alt="返回" />
      </div>
      <div class="page-title">{{ pageTitle }}</div>
    </div>

    <!-- 二维码区域:绑定跨端长按事件 -->
    <div class="qrcode-wrapper">
      <canvas
        ref="qrcodeCanvas"
        class="qrcode-canvas"
        <!-- PC端鼠标事件 -->
        @mousedown="handlePressStart"
        @mouseup="handlePressEnd"
        @mouseleave="handlePressEnd"
        <!-- 移动端触摸事件 -->
        @touchstart="handlePressStart"
        @touchend="handlePressEnd"
        @touchcancel="handlePressEnd"
      ></canvas>
      <!-- 长按提示:提升用户感知 -->
      <div class="press-tip" v-show="isPressing">长按1秒保存二维码</div>
    </div>
  </div>
</template>

三、功能层实现:核心逻辑拆解(含双模式适配)

1. 基础配置与变量定义

通过 Props 灵活配置「是否显示背景图」「页面标题」「二维码内容」等核心参数,适配不同业务场景:

javascript

运行

import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import QRCode from 'qrcode';

// 定义通用Props,支持外部传参配置
const props = defineProps({
  // 是否显示背景图
  showBg: {
    type: Boolean,
    default: true // 默认显示背景图
  },
  // 页面标题
  pageTitle: {
    type: String,
    default: '二维码分享'
  },
  // 二维码内容(支持外部传入)
  qrContent: {
    type: String,
    default: 'https://example.com'
  },
  // 二维码尺寸比例(视口占比)
  qrScale: {
    type: Number,
    default: 0.55
  }
});

// 基础变量定义
const qrcodeCanvas = ref(null); // Canvas元素引用
const router = useRouter();
const route = useRoute();
let resizeHandler = null; // resize事件处理函数(用于卸载清理)

// 长按相关变量
const longPressTimer = ref(null);
const LONG_PRESS_DURATION = 1000; // 长按触发时长(1秒)
const isPressing = ref(false); // 长按状态标识

2. 自适应二维码生成(兼容双模式)

核心逻辑支持「带背景图」和「无背景图」下的二维码自适应生成,保证不同视觉模式下的扫码体验:

javascript

运行

// 核心:生成自适应二维码
const createQRCode = async () => {
  // 校验Canvas元素是否存在
  if (!qrcodeCanvas.value) {
    console.warn('Canvas元素未挂载,跳过二维码生成');
    return;
  }

  try {
    // 1. 动态计算二维码尺寸:基于视口比例,兼容所有屏幕
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;
    const qrBaseSize = Math.min(viewportWidth, viewportHeight);
    const qrSize = qrBaseSize * props.qrScale;

    // 2. 调整容器尺寸,避免Canvas拉伸变形
    const container = document.querySelector('.qrcode-wrapper');
    if (container) {
      container.style.width = `${qrSize}px`;
      container.style.height = `${qrSize}px`;
    }

    // 3. 配置二维码参数(通用型配置,适配双模式)
    const qrOptions = {
      width: qrSize,
      margin: 3, // 边距:避免内容贴边,提升扫码成功率
      errorCorrectionLevel: 'H', // 高容错级别:适配各类场景
      color: {
        dark: '#000000', // 二维码深色块(保证对比度)
        light: props.showBg ? '#ffffff' : '#f8f8f8' // 无背景时匹配页面底色
      }
    };

    // 4. 优先使用路由参数,其次使用Props,最后用默认值
    const finalQrContent = route.query.qrUrl || props.qrContent;

    // 5. 异步绘制二维码到Canvas
    await QRCode.toCanvas(qrcodeCanvas.value, finalQrContent, qrOptions);
  } catch (error) {
    console.error('二维码生成失败:', error);
    alert('二维码加载失败,请刷新页面重试');
  }
};

// 路由返回功能(通用兼容)
const handleGoBack = () => {
  router.go(-1).catch(() => {
    router.push('/'); // 无历史记录时返回首页
  });
};

// 页面生命周期:挂载+卸载
onMounted(async () => {
  await nextTick(); // 等待DOM完全挂载
  createQRCode();

  // 监听窗口尺寸变化(添加节流,优化性能)
  resizeHandler = throttle(() => {
    createQRCode();
  }, 100);
  window.addEventListener('resize', resizeHandler);
});

onUnmounted(() => {
  // 清除事件监听,避免内存泄漏
  if (resizeHandler) {
    window.removeEventListener('resize', resizeHandler);
  }
});

// 原生节流函数(无需引入第三方库)
function throttle(fn, delay) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

3. 跨端长按下载(双模式下统一体验)

无论是否显示背景图,长按下载逻辑保持一致,同时兼容不同浏览器的下载限制:

javascript

运行

// 开始长按:启动定时器
const handlePressStart = () => {
  isPressing.value = true;
  longPressTimer.value = setTimeout(() => {
    downloadQRCode();
    isPressing.value = false;
  }, LONG_PRESS_DURATION);
};

// 取消长按:清除定时器
const handlePressEnd = () => {
  isPressing.value = false;
  if (longPressTimer.value) {
    clearTimeout(longPressTimer.value);
    longPressTimer.value = null;
  }
};

// 核心:下载二维码图片(兼容双模式)
const downloadQRCode = () => {
  if (!qrcodeCanvas.value) {
    alert('二维码未加载完成,无法下载');
    return;
  }

  try {
    // 1. 将Canvas转换为PNG格式(质量1.0)
    const qrDataUrl = qrcodeCanvas.value.toDataURL('image/png', 1.0);

    // 2. 创建隐藏a标签触发下载
    const downloadLink = document.createElement('a');
    // 自定义文件名:通用命名规则,适配多场景
    downloadLink.download = `二维码-${new Date().getTime()}.png`;
    downloadLink.href = qrDataUrl;

    // 3. 触发下载并清理DOM
    document.body.appendChild(downloadLink);
    downloadLink.click();
    document.body.removeChild(downloadLink);

    // 4. 下载成功反馈
    alert('二维码已保存到相册/下载文件夹');
  } catch (error) {
    console.error('二维码下载失败:', error);
    // 降级提示:适配微信内置浏览器等禁止DataURL下载的场景
    alert('下载失败,请长按二维码截图保存');
  }
};

四、双模式使用示例

1. 带背景图模式(默认)

适用于景区、活动等需要视觉氛围的场景,直接使用默认配置即可:

vue

<!-- 父组件中使用 -->
<QRCodePage 
  page-title="景区门票二维码"
  qr-content="https://example.com/ticket/123456"
/>

2. 无背景图模式

适用于电商、工具类等简约风格场景,通过showBg关闭背景图:

vue

<!-- 父组件中使用 -->
<QRCodePage 
  :show-bg="false"
  page-title="商品分享二维码"
  qr-content="https://example.com/goods/789012"
  :qr-scale="0.6" // 调整二维码尺寸比例
/>

五、体验优化与兼容性兜底

1. 视觉反馈增强

添加长按状态提示,让用户清晰感知操作进度,双模式下样式统一:

css

/* 长按提示文字:通用样式,适配有无背景 */
.press-tip {
  position: absolute;
  bottom: -30px;
  font-size: 12px;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.7);
  padding: 4px 12px;
  border-radius: 12px;
  pointer-events: none; /* 避免遮挡点击事件 */
}

/* 无背景图时调整提示文字颜色,保证可读性 */
.top:not(.has-bg) .press-tip {
  color: #333;
  background-color: rgba(255, 255, 255, 0.9);
  border: 1px solid #eee;
}

2. 兼容性优化

  • 路由兼容:返回操作失败时跳转到首页,避免页面卡死;
  • 下载兼容:部分浏览器(如微信内置浏览器)禁止 DataURL 下载,提示截图保存;
  • 尺寸兼容:二维码浅色块颜色随背景模式调整,保证对比度;
  • 性能兼容:resize 事件节流处理,避免频繁重绘导致卡顿。

六、总结与最佳实践

1. 核心收获

  • 模式灵活:通过简单配置项实现「有 / 无背景图」快速切换,无需重构核心逻辑;
  • 适配通用:基于视口的动态尺寸计算,适配所有移动端设备,无固定像素值;
  • 交互统一:跨端长按逻辑兼容 PC / 移动端,模拟原生 APP 体验;
  • 复用性强:通过 Props 配置核心参数,可直接复用至景区、电商、活动等多场景。

2. 最佳实践

  • 二维码容错级别建议设为 H:高容错可提升扫码成功率,适配各类复杂场景;
  • 背景图模式下,二维码浅色块固定为白色:保证与背景图的对比度,避免扫码困难;
  • 无背景图模式下,二维码浅色块匹配页面底色:保持视觉简约统一;
  • 长按时长控制在 1 秒:平衡误触率与用户体验;
  • 生产环境建议添加二维码内容校验:避免无效链接导致生成失败。

该组件通过「配置化 + 通用化」设计,兼顾了视觉灵活性与功能稳定性,可作为移动端 H5 二维码场景的通用解决方案,只需调整 Props 参数和样式细节,即可快速适配不同业务需求。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐