使用zxing实现扫码功能

1.安装zxing

"devDependencies": {
    "@zxing/library": "^0.21.3",
}

 

2.手机端页面使用示例(该扫码功能也可用于PC端扫码)

<template>
  <div :class="subFormItem ? 'sub-comp' : 'custom-comp'">
    <center>
      <van-icon name="scan" class="scan-icon" @click="startScan" />
      <div>{{ item.placeholder }}</div>
    </center>
    <!-- 添加扫码界面容器 -->
    <div v-if="isScanning" class="scan-container">
      <div class="scan-mask"></div>
      <div class="scan-box">
        <video id="videoElement" class="scan-video"></video>
        <div class="scan-area"></div>
        <div class="scan-controls">
          <van-button type="primary" @click="stopScan">取消</van-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import {
  onMounted,
  ref,
  toRefs,
  defineAsyncComponent,
  computed,
  watch,
  onUnmounted,
} from "vue";
import { Toast, Dialog } from "vant";
import { BrowserQRCodeReader, NotFoundException } from "@zxing/library";
import TaskApi from "@/modules/new_collect/api/taskApi";
import { useRouter, useRoute } from "vue-router";

const route = useRoute();
const router = useRouter();
const props = defineProps({
  formData: Object,
  item: Object,
  isReadOnly: Boolean,
  wholeFormData: Object,
  subFormItem: Object,
  subFormIdx: Number,
});
const { formData, item, isReadOnly, wholeFormData, subFormItem, subFormIdx } =
  toRefs(props);
const isCompReadOnly = ref(false);
const isScanning = ref(false); // 扫码状态
let codeReader = null; // 扫码器实例

onMounted(() => {
  isCompReadOnly.value = isReadOnly.value || item.value.isReadOnly;
});

// 清理扫码资源
onUnmounted(() => {
  stopScan();
});

// 开始扫码
const startScan = () => {
  if (isCompReadOnly.value) return;

  isScanning.value = true;

  // 延迟执行,等待DOM渲染完成
  setTimeout(() => {
    codeReader = new BrowserQRCodeReader();
    const videoInputDeviceId = null; // 可选:指定摄像头ID

    codeReader.decodeFromVideoDevice(
      videoInputDeviceId,
      "videoElement",
      (result, err) => {
        if (result) {
          console.log("扫码结果:", result.text);
          formData.value[item.value.prop] = result.text;
          Toast.success("扫码成功");
          stopScan();
          getOptions();
        }
        if (err && !(err instanceof NotFoundException)) {
          console.error(err);
          Toast.fail("扫码失败,请重试");
        }
      }
    );
  }, 100);
};

// 停止扫码
const stopScan = () => {
  isScanning.value = false;

  // 释放摄像头资源
  if (codeReader) {
    codeReader.reset();
    codeReader = null;
  }
};
const dataSource = ref();
const fieldOptions = ref([]);
const showDropdownPicker = ref(false);
const getOptions = async () => {
  if (!item.value.sheetId || !item.value.field) {
    return;
  }
  const { data } = await TaskApi.getSheetAndFieldByRules(
    { sheetName: item.value.sheetId },
    [
      {
        field: item.value.field,
        criteria: "=",
        content: formData.value[item.value.prop],
      },
    ]
  );
  if (data.code !== 200) {
    Toast.fail(data.message);
    return;
  }
  // showDropdownPicker.value = true;
  dataSource.value = data.data.map((v) => {
    return {
      ...v,
      checked: false,
    };
  });
  sessionStorage.setItem("formData", JSON.stringify(formData.value));
  if (dataSource.value.length > 1) {
    router.push("/ScanCodeBindSelect/list");
    sessionStorage.setItem(
      "currentDataSource",
      JSON.stringify(dataSource.value)
    );
  } else if (dataSource.value.length == 1) {
    dataSource.value[0].checked = true;
    dataSource.value.forEach((v) => {
      if (v.checked) {
        Object.keys(item.value.fieldRelation).forEach((key) => {
          Object.keys(v).forEach((g) => {
            if (g == key) {
              props.formData[item.value.fieldRelation[key]] = v[g];
            }
          });
        });
      }
    });
    const formData = sessionStorage.getItem("formData")
      ? JSON.parse(sessionStorage.getItem("formData"))
      : {};
    for (let k in formData) {
      props.formData[k] = formData[k];
    }
    sessionStorage.removeItem("currentDataSource");
    sessionStorage.removeItem("formData");
  }

  fieldOptions.value = data.data.map((item) => item.id);
};

watch(
  () => sessionStorage.getItem("currentDataSource"),
  (val) => {
    if (val) {
      if (sessionStorage.getItem("currentDataSource")) {
        let dataSource = JSON.parse(val);
        if (Array.isArray(dataSource))
          dataSource &&
            dataSource.forEach((v) => {
              if (v.checked) {
                Object.keys(item.value.fieldRelation).forEach((key) => {
                  Object.keys(v).forEach((g) => {
                    if (g == key) {
                      props.formData[item.value.fieldRelation[key]] = v[g];
                    }
                  });
                });
              }
            });
        const formData = sessionStorage.getItem("formData")
          ? JSON.parse(sessionStorage.getItem("formData"))
          : {};
        for (let k in formData) {
          props.formData[k] = formData[k];
        }
        sessionStorage.removeItem("currentDataSource");
        sessionStorage.removeItem("formData");
      }
    }
  },
  {
    immediate: true,
    deep: true,
  }
);

const onDropdownConfirm = (val) => {
  console.log(val);
  showDropdownPicker.value = false;
};
</script>

<style scoped lang="less">
.custom-comp {
  margin: 0px 20px;
  width: 90%;
}
.component {
  .custom-comp {
    margin: 0;
    width: 100%;
  }
}

/* 扫码界面样式 */
.scan-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
}

.scan-mask {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.7);
}

.scan-box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80%;
  max-width: 400px;
  background-color: #fff;
  border-radius: 8px;
  overflow: hidden;
}

.scan-video {
  width: 100%;
  height: auto;
}

.scan-area {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 200px;
  height: 200px;
  border: 2px solid #409eff;
  box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.5);
  background-color: transparent;
}

.scan-controls {
  padding: 15px;
  text-align: center;
}
.list {
  padding-bottom: 60px;
}
.list-item {
  margin: 16px 16px 0 16px;
  border-radius: 8px;
  width: auto;
}
.scan-icon {
  font-size: 80px;
}
</style>

tips:必须是localhost或https才能使用扫码功能

posted @ 2025-07-14 11:21  罗毅豪  阅读(105)  评论(0)    收藏  举报