vue移动应用拍照和扫码解决方案

23 阅读3分钟

Vue 移动应用条形码扫描与拍照集成方案

下面是一个完整的 Vue 移动应用实现方案,包含条形码扫描和拍照功能,并正确处理手机相机资源。

方案一:使用 Cordova/Capacitor 插件(适合混合应用)

1. 安装必要插件

# 对于 Capacitor 项目
npm install @capacitor/camera @capacitor/barcode-scanner
npx cap sync

# 对于 Cordova 项目
cordova plugin add phonegap-plugin-barcodescanner
cordova plugin add cordova-plugin-camera

2. 实现代码

<template>
  <div class="scanner-container">
    <!-- 扫描界面 -->
    <div v-if="mode === 'scan'" class="mode-container">
      <button @click="startScan">开始扫描条形码</button>
      <p v-if="scanResult">扫描结果: {{ scanResult }}</p>
    </div>
    
    <!-- 拍照界面 -->
    <div v-if="mode === 'camera'" class="mode-container">
      <div class="camera-preview">
        <img v-if="capturedImage" :src="capturedImage" class="preview-image">
        <video v-else ref="videoElement" autoplay playsinline class="camera-view"></video>
      </div>
      
      <div class="controls">
        <button @click="takePicture" v-if="!capturedImage">拍照</button>
        <button @click="retakePicture" v-if="capturedImage">重拍</button>
        <button @click="confirmPicture" v-if="capturedImage">确认照片</button>
        <button @click="closeCamera">返回</button>
      </div>
    </div>
  </div>
</template>

<script>
import { BarcodeScanner } from '@capacitor-community/barcode-scanner';
import { Camera } from '@capacitor/camera';

export default {
  data() {
    return {
      mode: 'scan', // 'scan' 或 'camera'
      scanResult: '',
      capturedImage: '',
      mediaStream: null,
      currentBarcode: null
    };
  },
  methods: {
    // 条形码扫描方法
    async startScan() {
      try {
        // 请求相机权限
        await BarcodeScanner.checkPermission({ force: true });
        
        // 隐藏页面内容(全屏扫描)
        document.querySelector('body').classList.add('scanner-active');
        
        const result = await BarcodeScanner.startScan();
        
        if (result.hasContent) {
          this.scanResult = result.content;
          this.currentBarcode = result.content;
          this.mode = 'camera'; // 扫描成功后切换到拍照模式
        }
      } catch (error) {
        console.error('扫描错误:', error);
        alert('扫描失败: ' + error.message);
      } finally {
        // 恢复页面显示
        BarcodeScanner.showBackground();
        document.querySelector('body').classList.remove('scanner-active');
      }
    },
    
    // 初始化相机
    async initCamera() {
      try {
        this.mediaStream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: 'environment', // 使用后置摄像头
            width: { ideal: 1280 },
            height: { ideal: 720 }
          }
        });
        
        this.$refs.videoElement.srcObject = this.mediaStream;
      } catch (error) {
        console.error('相机初始化失败:', error);
        alert('无法访问相机: ' + error.message);
      }
    },
    
    // 拍照
    async takePicture() {
      try {
        // 使用 Capacitor Camera 插件
        const image = await Camera.getPhoto({
          quality: 90,
          allowEditing: false,
          resultType: 'dataUrl',
          direction: 'rear'
        });
        
        this.capturedImage = image.dataUrl;
        
        // 关闭视频流
        this.closeVideoStream();
      } catch (error) {
        console.error('拍照失败:', error);
      }
    },
    
    // 重拍
    retakePicture() {
      this.capturedImage = '';
      this.initCamera();
    },
    
    // 确认照片
    confirmPicture() {
      // 这里可以处理上传逻辑
      alert(`条形码: ${this.currentBarcode}\n照片已保存`);
      this.resetProcess();
    },
    
    // 关闭相机
    closeCamera() {
      this.closeVideoStream();
      this.resetProcess();
    },
    
    // 关闭视频流
    closeVideoStream() {
      if (this.mediaStream) {
        this.mediaStream.getTracks().forEach(track => track.stop());
        if (this.$refs.videoElement) {
          this.$refs.videoElement.srcObject = null;
        }
        this.mediaStream = null;
      }
    },
    
    // 重置流程
    resetProcess() {
      this.capturedImage = '';
      this.currentBarcode = '';
      this.scanResult = '';
      this.mode = 'scan';
    }
  },
  beforeDestroy() {
    this.closeVideoStream();
  }
};
</script>

<style>
.scanner-container {
  padding: 20px;
}

.mode-container {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.camera-preview {
  width: 100%;
  max-width: 500px;
  margin: 20px 0;
  position: relative;
  aspect-ratio: 4/3;
  background-color: #000;
}

.camera-view, .preview-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

button {
  padding: 10px 20px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  cursor: pointer;
}

/* 扫描模式下的全屏样式 */
.scanner-active {
  --background: transparent;
  --ion-background-color: transparent;
}
</style>

方案二:纯 Web 实现(使用开源库)

如果不使用 Cordova/Capacitor,可以使用以下纯 Web 方案:

1. 安装依赖

npm install vue-qrcode-reader quagga

2. 实现代码

<template>
  <div>
    <!-- 条形码扫描 -->
    <div v-if="!showCamera">
      <button @click="startBarcodeScan">扫描条形码</button>
      <div ref="barcodeScanner" style="width: 100%; height: 300px;"></div>
      <p v-if="barcodeResult">扫描结果: {{ barcodeResult }}</p>
    </div>
    
    <!-- 拍照界面 -->
    <div v-if="showCamera">
      <video ref="videoElement" autoplay playsinline style="width: 100%;"></video>
      <button @click="captureImage">拍照</button>
      <button @click="stopCamera">取消</button>
      <canvas ref="canvasElement" style="display: none;"></canvas>
      
      <div v-if="capturedImage">
        <img :src="capturedImage" style="max-width: 100%;">
        <button @click="savePhoto">保存照片</button>
        <button @click="retakePhoto">重拍</button>
      </div>
    </div>
  </div>
</template>

<script>
import Quagga from 'quagga';

export default {
  data() {
    return {
      showCamera: false,
      barcodeResult: '',
      capturedImage: '',
      mediaStream: null
    };
  },
  methods: {
    // 条形码扫描
    startBarcodeScan() {
      Quagga.init({
        inputStream: {
          name: "Live",
          type: "LiveStream",
          target: this.$refs.barcodeScanner,
          constraints: {
            width: 480,
            height: 320,
            facingMode: "environment"
          },
        },
        decoder: {
          readers: ["ean_reader", "ean_8_reader", "code_128_reader"]
        },
      }, err => {
        if (err) {
          console.error(err);
          return;
        }
        Quagga.start();
      });

      Quagga.onDetected(result => {
        this.barcodeResult = result.codeResult.code;
        Quagga.stop();
        this.showCamera = true;
        this.initCamera();
      });
    },
    
    // 初始化相机
    async initCamera() {
      try {
        this.mediaStream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: 'environment',
            width: { ideal: 1280 },
            height: { ideal: 720 }
          }
        });
        this.$refs.videoElement.srcObject = this.mediaStream;
      } catch (error) {
        console.error('相机错误:', error);
      }
    },
    
    // 拍照
    captureImage() {
      const video = this.$refs.videoElement;
      const canvas = this.$refs.canvasElement;
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      
      const ctx = canvas.getContext('2d');
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      
      this.capturedImage = canvas.toDataURL('image/jpeg', 0.9);
      this.closeVideoStream();
    },
    
    // 保存照片
    savePhoto() {
      // 这里可以处理上传逻辑
      alert(`条形码: ${this.barcodeResult}\n照片已保存`);
      this.resetProcess();
    },
    
    // 重拍
    retakePhoto() {
      this.capturedImage = '';
      this.initCamera();
    },
    
    // 停止相机
    stopCamera() {
      this.closeVideoStream();
      this.resetProcess();
    },
    
    // 关闭视频流
    closeVideoStream() {
      if (this.mediaStream) {
        this.mediaStream.getTracks().forEach(track => track.stop());
        this.$refs.videoElement.srcObject = null;
        this.mediaStream = null;
      }
    },
    
    // 重置流程
    resetProcess() {
      this.capturedImage = '';
      this.barcodeResult = '';
      this.showCamera = false;
    }
  },
  beforeDestroy() {
    Quagga.stop();
    this.closeVideoStream();
  }
};
</script>

关键注意事项

  1. 相机权限管理

    • 确保应用有相机权限
    • 处理用户拒绝权限的情况
  2. 资源释放

    • 每次切换模式时释放之前的相机资源
    • 在组件销毁时确保释放所有资源
  3. 移动设备优化

    • 使用 facingMode: 'environment' 强制使用后置摄像头
    • 添加 playsinline 属性确保在 iOS 上正常工作
  4. 性能考虑

    • 限制相机分辨率以避免性能问题
    • 及时停止扫描器和相机以节省电量
  5. 用户体验

    • 提供清晰的界面状态指示
    • 添加加载状态和错误反馈

以上两种方案都可以在 Vue 移动应用中实现先扫描条形码再拍照的功能,第一种方案更适合打包成原生应用,第二种方案则适合纯 Web 应用。